Files
OpenClaw Sub-agent 595b99446b feat: ⌘K command palette + restore green nav (fixes mobile menu collapse)
- Command palette (Ctrl/Cmd+K or nav search icon): /search/palette/ groups
  links/pages/posts with jump/detail/edit actions, keyboard navigation
- Hero: stats inline under search pill, ⌘K chip on desktop, Random Images
  broken-image fallback to elegant SVG placeholder (layout untouched)
- Restore original green nav (bg-custom-blue). The frosted-glass nav from
  1.0.401 broke the mobile Menu dropdown: backdrop-filter creates a
  containing block, collapsing the fixed full-screen dropdown to 3px
- Palette tests: +5 cases (grouping, post match, management fields,
  empty query, no results)
2026-08-02 11:20:11 +10:00

650 lines
41 KiB
HTML

{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans "GoLinks" %}</title>
<link href="{% static 'css/dist/styles.css' %}" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.7.2/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link rel="shortcut icon" type="image/x-icon" href="{% static 'favicon.ico' %}">
<style>
#search-results {
background-color: white;
border: 1px solid #e2e8f0;
border-top: none;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
max-height: 200px;
overflow-y: auto;
}
#search-results div {
padding: 0.5rem 1rem;
cursor: pointer;
color: #4a5568;
}
#search-results div:hover, #search-results div.active {
background-color: #e2e8f0;
}
/* Scroll-responsive navigation styles.
IMPORTANT: do NOT animate the nav with `transform`. Any non-`none`
transform on an ancestor makes it the containing block for its
`position: fixed` descendants, which breaks the mobile menu dropdown
(the dropdown uses `fixed inset-0` on mobile to overlay the viewport).
We animate `top` instead. */
.nav-hidden {
top: -80px;
}
.nav-visible {
top: 0;
}
nav {
transition: top 0.3s ease-in-out;
}
/* Animated nav dropdowns.
We toggle a `.menu-open` class instead of `hidden` so that CSS
opacity/transform transitions run on open and close. */
#more-menu-dropdown,
#language-options {
opacity: 0;
pointer-events: none;
transform: translateY(-10px);
transition: opacity 0.22s ease, transform 0.22s cubic-bezier(0.28, 0.11, 0.32, 1);
}
#more-menu-dropdown.menu-open,
#language-options.menu-open {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
/* Mobile: full-screen overlay slides down from the top */
@media (max-width: 767px) {
#more-menu-dropdown {
transform: translateY(-12px);
transition: opacity 0.28s ease, transform 0.32s cubic-bezier(0.28, 0.11, 0.32, 1);
}
}
@media (prefers-reduced-motion: reduce) {
#more-menu-dropdown,
#language-options,
#more-menu-dropdown.menu-open,
#language-options.menu-open {
transition: none;
transform: none;
}
}
/* Line clamp utilities for truncating text */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ===================== ⌘K command palette ===================== */
.palette-overlay {
position: fixed; inset: 0; z-index: 1000;
display: flex; align-items: flex-start; justify-content: center;
padding: min(12vh, 6rem) 1rem 2rem;
background: rgba(0, 0, 0, 0.38);
-webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
animation: palette-fade 0.18s ease;
}
.palette-overlay[hidden] { display: none; }
.palette {
width: 100%; max-width: 36rem;
background: rgba(255, 255, 255, 0.96);
-webkit-backdrop-filter: blur(24px) saturate(180%); backdrop-filter: blur(24px) saturate(180%);
border-radius: 22px;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.28);
overflow: hidden;
animation: palette-pop 0.22s cubic-bezier(0.28, 0.11, 0.32, 1);
}
.palette-search {
display: flex; align-items: center; gap: 0.75rem;
padding: 1rem 1.25rem; border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.palette-search > svg { width: 1.15rem; height: 1.15rem; color: #86868b; flex: none; }
.palette-search input {
flex: 1; min-width: 0; border: none; outline: none;
font-size: 1.1rem; color: #1d1d1f; background: transparent;
}
.palette-search input::placeholder { color: #86868b; }
.palette-search kbd {
font-family: inherit; font-size: 0.72rem; color: #86868b;
background: rgba(0, 0, 0, 0.06); border-radius: 6px;
padding: 0.15rem 0.5rem; flex: none;
}
.palette-results { max-height: min(60vh, 30rem); overflow-y: auto; padding: 0.5rem; }
.palette-group-title {
font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.09em; color: #86868b;
padding: 0.7rem 0.9rem 0.3rem;
}
.palette-item {
display: flex; align-items: center; gap: 0.85rem;
padding: 0.7rem 0.9rem; border-radius: 14px; cursor: pointer;
}
.palette-item.active { background: rgba(0, 113, 227, 0.09); }
.palette-item-main { flex: 1; min-width: 0; }
.palette-item-title { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.palette-item-title a {
font-weight: 600; font-size: 0.98rem; color: #1d1d1f; text-decoration: none;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.palette-item-title a:hover { color: #0071e3; }
.palette-item-sub {
font-size: 0.8rem; color: #86868b;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.palette-item mark {
background: rgba(0, 113, 227, 0.14); color: #0071e3;
border-radius: 4px; padding: 0 1px;
}
.palette-badge {
font-size: 0.68rem; font-weight: 600; padding: 0.15rem 0.55rem;
border-radius: 980px; flex: none; white-space: nowrap;
}
.palette-badge.link { background: rgba(0, 113, 227, 0.10); color: #0071e3; }
.palette-badge.action, .palette-badge.template { background: rgba(191, 90, 242, 0.12); color: #8944ab; }
.palette-badge.custom { background: rgba(48, 209, 88, 0.15); color: #1e7b34; }
.palette-item-actions { display: flex; align-items: center; gap: 0.15rem; flex: none; }
.palette-item-actions a {
display: inline-flex; align-items: center; justify-content: center;
width: 34px; height: 34px; border-radius: 50%;
color: #8e8e93; text-decoration: none;
transition: background 0.2s ease, color 0.2s ease;
}
.palette-item-actions a:hover { background: rgba(0, 0, 0, 0.05); }
.palette-item-actions a.blue:hover { color: #0071e3; }
.palette-item-actions a.green:hover { color: #1e7b34; }
.palette-item-actions svg { width: 1.05rem; height: 1.05rem; }
.palette-empty { padding: 2rem 1rem; text-align: center; color: #86868b; font-size: 0.92rem; }
.palette-footer {
display: flex; align-items: center; justify-content: center; gap: 1.1rem;
padding: 0.6rem 1rem; border-top: 1px solid rgba(0, 0, 0, 0.05);
font-size: 0.72rem; color: #86868b;
}
.palette-footer kbd {
font-family: inherit; font-size: 0.7rem;
background: rgba(0, 0, 0, 0.06); border-radius: 5px;
padding: 0.1rem 0.4rem;
}
@keyframes palette-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes palette-pop {
from { opacity: 0; transform: translateY(-10px) scale(0.98); }
to { opacity: 1; transform: none; }
}
@media (max-width: 640px) {
.palette-overlay { padding-top: 4rem; align-items: flex-start; }
.palette-footer { display: none; }
}
</style>
{{ form.media }}
{% block extra_css %}{% endblock %}
{% block extra_head %}{% endblock %}
<script src="{% static 'js/common.js' %}"></script>
</head>
<body class="bg-gray-100">
<nav id="main-nav" class="bg-custom-blue text-white p-4 fixed w-full top-0 z-50 nav-visible">
<div class="container mx-auto flex justify-between items-center">
<a href="{% url 'link_list' %}" class="text-xl font-bold">{% trans "GoLinks" %}</a>
<div class="flex items-center space-x-4">
<!-- ⌘K search trigger -->
<button id="palette-trigger" class="flex items-center text-white hover:text-gray-200" aria-label="{% trans 'Search' %} (⌘K)" title="{% trans 'Search' %} (⌘K)">
<i class="fas fa-search"></i>
</button>
<!-- "Menus" 下拉菜单 -->
<div class="relative group">
<button id="more-menu-button" class="flex items-center text-white hover:text-gray-200">
<i class="fas fa-bars mr-2"></i>
{% trans "Menu" %}
</button>
<div id="more-menu-dropdown" class="fixed inset-0 top-[57px] w-full rounded-none overflow-y-auto md:absolute md:inset-auto md:top-auto md:right-0 md:mt-2 md:w-48 md:rounded-md md:overflow-visible bg-white shadow-xl z-20">
<div class="grid grid-cols-3 gap-0.5 p-1.5 md:grid-cols-1 md:gap-0 md:p-0">
<a href="{% url 'search' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Search" %}</span>
</a>
<a href="{% url 'page-list' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9.5a2 2 0 00-2-2h-2"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Pages" %}</span>
</a>
<a href="{% url 'bookmark-list' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-5-7 5V5z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Bookmarks" %}</span>
</a>
<a href="{% url 'post-list' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-5 h-5 mb-1 md:w-4 md:h-4 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9.5a2 2 0 00-2-2h-2"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Posts" %}</span>
</a>
<a href="{% url 'screenshot-gallery' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Screenshots" %}</span>
</a>
<a href="{% url 'collection-list' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Images" %}</span>
</a>
<a href="{% url 'tag-list' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Tags" %}</span>
</a>
<a href="{% url 'api-docs' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-1.5 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "APIs" %}</span>
</a>
<a href="{% url 'netscan-dashboard' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "NetScan" %}</span>
</a>
<a href="{% url 'nginxmon-dashboard' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Nginx Monitor" %}</span>
</a>
<a href="{% url 'pricemon-dashboard' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Price Monitor" %}</span>
</a>
<a href="{% url 'routermon-dashboard' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.142 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Router Monitor" %}</span>
</a>
<a href="{% url 'invest-dashboard' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Invest" %}</span>
</a>
<a href="/jbot/" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "JBOT" %}</span>
</a>
<a href="{% url 'file-list' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Files" %}</span>
</a>
<a href="{% url 'mini-apps-list' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 11H5m14-4H5a2 2 0 00-2 2v6a2 2 0 002 2h14M9 7h1m4 0h1M9 17h1m4 0h1"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Mini Apps" %}</span>
</a>
<a href="{% url 'tools' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Tools" %}</span>
</a>
<a href="{% url 'help' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Help" %}</span>
</a>
<a href="{% url 'jobs' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Jobs" %}</span>
</a>
<a href="{% url 'site-settings' %}" class="flex flex-col items-center justify-center p-2.5 rounded-lg md:flex-row md:justify-start md:px-4 md:py-2 md:rounded-none transition-colors text-gray-700 hover:bg-gray-100">
<svg class="w-6 h-6 mb-1 md:w-5 md:h-5 md:mr-2 md:mb-0 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
<span class="text-[11px] leading-tight text-center md:text-sm md:text-left">{% trans "Settings" %}</span>
</a>
</div>
</div>
</div>
<!-- 语言选择器 -->
<div class="relative">
<button id="language-selector-button" class="flex items-center text-white hover:text-gray-200">
<i class="fas fa-globe mr-2"></i>
{% get_current_language as LANGUAGE_CODE %}
{% get_language_info for LANGUAGE_CODE as current_language %}
{{ current_language.code|upper }}
</button>
<div id="language-options" class="absolute right-0 mt-2 py-2 w-48 bg-white rounded-md shadow-xl z-20">
<form action="{% url 'set_language' %}" method="post" id="language-form">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.path }}">
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<button type="submit" name="language" value="{{ language.code }}" class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 {% if language.code == LANGUAGE_CODE %}font-bold{% endif %}">
{{ language.name_local }} ({{ language.code|upper }})
</button>
{% endfor %}
</form>
</div>
</div>
</div>
</div>
</nav>
<div class="container mx-auto mt-20 p-2">
{% if messages %}
{% for message in messages %}
<div class="flash-message bg-{{ message.tags }}-100 bg-yellow-100 border border-{{ message.tags }}-400 text-{{ message.tags }}-700 border-yellow-500 text-yellow-700 px-4 py-3 rounded relative mb-4 transition-all duration-500 ease-in-out" role="alert">
<span class="block sm:inline">{{ message }}</span>
</div>
{% endfor %}
{% endif %}
{% block content %}
<h1 class="text-2xl font-bold mb-4">Welcome to GoLinks</h1>
<p>If you can see this message, the base template is working correctly.</p>
{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.9/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
<script>
// Pass Django CSRF token to all HTMX requests automatically
document.addEventListener('htmx:configRequest', function(e) {
var token = document.cookie.match(/csrftoken=([^;]+)/)?.[1];
if (token) e.detail.headers['X-CSRFToken'] = token;
});
</script>
<script src="{% static 'js/navbar.js' %}"></script>
<script>
$(document).ready(function() {
$('.select2').select2({
theme: 'classic',
width: '100%'
});
// Auto-dismiss flash messages after 5s with fade + collapse animation
setTimeout(function() {
$('.flash-message').each(function() {
$(this).css({'opacity': '0', 'max-height': $(this).outerHeight(true) + 'px', 'overflow': 'hidden', 'margin-bottom': $(this).css('margin-bottom')});
$(this).animate({'opacity': 0, 'max-height': 0, 'marginBottom': 0, 'paddingTop': 0, 'paddingBottom': 0}, 500, function() {
$(this).remove();
});
});
}, 5000);
});
</script>
{% block extra_js %}{% endblock %}
<!-- ===================== ⌘K command palette ===================== -->
<div id="palette-overlay" class="palette-overlay" hidden>
<div class="palette" role="dialog" aria-modal="true" aria-label="{% trans 'Search' %}">
<div class="palette-search">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-4.35-4.35M11 19a8 8 0 100-16 8 8 0 000 16z"/></svg>
<input id="palette-input" type="search" placeholder="{% trans 'Search links, pages, posts…' %}" autocomplete="off" role="combobox" aria-expanded="false" aria-controls="palette-results">
<kbd>esc</kbd>
</div>
<div id="palette-results" class="palette-results" role="listbox"></div>
<div class="palette-footer">
<span><kbd>↑↓</kbd> navigate</span>
<span><kbd></kbd> open</span>
<span><kbd></kbd> details</span>
<span><kbd>e</kbd> edit</span>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var overlay = document.getElementById('palette-overlay');
var input = document.getElementById('palette-input');
var results = document.getElementById('palette-results');
var trigger = document.getElementById('palette-trigger');
if (!overlay || !input || !results) return;
var items = []; // flat list of { el, primary, detail, edit, isLink }
var focus = -1;
var escapeHtml = function (s) {
return String(s || '').replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
};
var escapeRegExp = function (s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};
function highlight(text, query) {
var safe = escapeHtml(text);
if (!query) return safe;
try {
return safe.replace(new RegExp('(' + escapeRegExp(escapeHtml(query)) + ')', 'gi'), '<mark>$1</mark>');
} catch (e) { return safe; }
}
function badgeFor(item) {
var cls = 'palette-badge link', label = 'Link';
if (item.original_url && item.original_url.indexOf('{') > -1 && item.original_url.indexOf('}') > -1) { cls = 'palette-badge template'; label = 'Template'; }
else if (item.link_type === 'ACTION') { cls = 'palette-badge action'; label = 'Action'; }
else if (item.link_type === 'CUSTOM') { cls = 'palette-badge custom'; label = 'Custom'; }
return '<span class="' + cls + '">' + label + '</span>';
}
var eyeSvg = '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>';
var pencilSvg = '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>';
function iconLink(href, cls, svg, label) {
var a = document.createElement('a');
a.href = href;
a.className = cls;
a.title = label;
a.setAttribute('aria-label', label);
a.innerHTML = svg;
return a;
}
function renderGroup(title, list, renderFn, q) {
if (!list || !list.length) return;
var gt = document.createElement('div');
gt.className = 'palette-group-title';
gt.textContent = title;
results.appendChild(gt);
list.forEach(function (item) { renderFn(item, q); });
}
function addRow(mainInnerHtml, subText, actions, entry) {
var row = document.createElement('div');
row.className = 'palette-item';
row.setAttribute('role', 'option');
var main = document.createElement('div');
main.className = 'palette-item-main';
main.innerHTML = mainInnerHtml;
if (subText) {
var sub = document.createElement('div');
sub.className = 'palette-item-sub';
sub.textContent = subText;
main.appendChild(sub);
}
row.appendChild(main);
var acts = document.createElement('div');
acts.className = 'palette-item-actions';
acts.appendChild(iconLink(entry.detail, 'blue', eyeSvg, 'Details'));
acts.appendChild(iconLink(entry.edit, 'green', pencilSvg, 'Edit'));
row.appendChild(acts);
row.addEventListener('click', function (e) {
if (e.target.closest('a')) return; // let links/actions handle it
window.location.href = entry.detail;
});
results.appendChild(row);
items.push({ el: row, primary: entry.primary, detail: entry.detail, edit: entry.edit, isLink: entry.isLink });
}
function render(data, q) {
results.innerHTML = '';
items = [];
focus = -1;
if (!data) return;
renderGroup('Links', data.links, function (item) {
var inner = '<div class="palette-item-title"><a href="' + escapeHtml(item.url) + '" target="_blank" rel="noopener noreferrer">' +
highlight(item.alias, q) + '</a>' + badgeFor(item) + '</div>';
addRow(inner, item.original_url || item.description, null,
{ primary: item.url, detail: item.detail_url, edit: item.edit_url, isLink: true });
}, q);
renderGroup('Pages', data.pages, function (item) {
var inner = '<div class="palette-item-title"><a href="' + escapeHtml(item.url) + '">' + highlight(item.title, q) + '</a></div>';
addRow(inner, item.original_url, null,
{ primary: item.url, detail: item.url, edit: item.edit_url, isLink: false });
}, q);
renderGroup('Posts', data.posts, function (item) {
var inner = '<div class="palette-item-title"><a href="' + escapeHtml(item.url) + '">' + highlight(item.title, q) + '</a></div>';
addRow(inner, null, null,
{ primary: item.url, detail: item.url, edit: item.edit_url, isLink: false });
}, q);
if (!items.length) {
var empty = document.createElement('div');
empty.className = 'palette-empty';
empty.textContent = '{% trans "No results found" %}';
results.appendChild(empty);
}
input.setAttribute('aria-expanded', items.length ? 'true' : 'false');
}
function fetchPalette(q) {
fetch('/search/palette/?q=' + encodeURIComponent(q))
.then(function (r) { return r.json(); })
.then(function (data) { render(data, q); })
.catch(function (err) { console.error('Palette search failed:', err); });
}
function setFocus(index) {
if (!items.length) return;
items.forEach(function (it, i) { it.el.classList.toggle('active', i === index); });
focus = index;
if (items[focus]) items[focus].el.scrollIntoView({ block: 'nearest' });
}
function move(delta) {
if (!items.length) return;
var next = ((focus + delta) % items.length + items.length) % items.length;
setFocus(next);
}
function openPalette() {
overlay.hidden = false;
input.value = '';
fetchPalette('');
setTimeout(function () { input.focus(); }, 60);
}
function closePalette() {
overlay.hidden = true;
}
// Triggers: nav button + ⌘K / Ctrl+K everywhere
if (trigger) trigger.addEventListener('click', function () { overlay.hidden ? openPalette() : closePalette(); });
document.addEventListener('keydown', function (e) {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
overlay.hidden ? openPalette() : closePalette();
}
});
// Overlay interactions
overlay.addEventListener('click', function (e) { if (e.target === overlay) closePalette(); });
input.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { closePalette(); }
else if (e.key === 'ArrowDown') { e.preventDefault(); move(1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1); }
else if (e.key === 'Enter') {
if (focus > -1 && items[focus]) {
e.preventDefault();
var it = items[focus];
if (it.isLink) window.open(it.primary, '_blank');
else window.location.href = it.primary;
closePalette();
}
}
else if (e.key === 'ArrowRight') {
if (focus > -1 && items[focus]) { e.preventDefault(); window.location.href = items[focus].detail; closePalette(); }
}
else if ((e.key === 'e' || e.key === 'E') && focus > -1 && items[focus] && !e.metaKey && !e.ctrlKey && !e.altKey) {
e.preventDefault(); window.location.href = items[focus].edit; closePalette();
}
});
var debounceTimer = null;
input.addEventListener('input', function () {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function () { fetchPalette(input.value.trim()); }, 200);
});
})();
</script>
{% if BUILD_TIME %}
<footer class="text-center text-gray-400 text-xs py-3 mt-4 border-t border-gray-100">
Built at: {{ BUILD_TIME }} with ver: {{ BUILD_VERSION }}
</footer>
{% endif %}
</body>
</html>