Add living styleguide endpoint and vanilla JS utilities
- New /styleguide endpoint with CSS Grid layout system, design tokens, and component library (buttons, forms, alerts, cards, tables, etc.) - custom.js replaces jQuery patterns with native DOM APIs - Pure CSS with custom properties, no preprocessors, no flexbox - Capability-driven presentation with progressive enhancement - Wire styleguide app into middleware includes - Update CLAUDE.md with UI modernization goals
This commit is contained in:
parent
4f73837822
commit
340e25aa71
7 changed files with 1979 additions and 0 deletions
11
rhodecode/apps/styleguide/__init__.py
Normal file
11
rhodecode/apps/styleguide/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
def includeme(config):
|
||||
from rhodecode.apps.styleguide.views import StyleGuideView
|
||||
|
||||
config.add_route(name="styleguide", pattern="/_admin/styleguide")
|
||||
config.add_view(
|
||||
StyleGuideView,
|
||||
attr="index",
|
||||
route_name="styleguide",
|
||||
request_method="GET",
|
||||
renderer=None,
|
||||
)
|
||||
21
rhodecode/apps/styleguide/views.py
Normal file
21
rhodecode/apps/styleguide/views.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import logging
|
||||
|
||||
from pyramid.renderers import render_to_response
|
||||
from rhodecode.apps._base import BaseAppView
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StyleGuideView(BaseAppView):
|
||||
def load_default_context(self):
|
||||
c = self._get_local_tmpl_context()
|
||||
return c
|
||||
|
||||
def index(self):
|
||||
c = self.load_default_context()
|
||||
c.active = "styleguide"
|
||||
return render_to_response(
|
||||
"styleguide/index.mako",
|
||||
self._get_template_context(c),
|
||||
request=self.request,
|
||||
)
|
||||
|
|
@ -383,6 +383,7 @@ def includeme(config, auth_resources=None):
|
|||
config.include("rhodecode.apps.svn_support.includeme")
|
||||
config.include("rhodecode.apps.ssh_support.includeme")
|
||||
config.include("rhodecode.apps.debug_style")
|
||||
config.include("rhodecode.apps.styleguide.includeme")
|
||||
|
||||
if load_all:
|
||||
config.include("rhodecode.integrations.includeme")
|
||||
|
|
|
|||
878
rhodecode/public/css/styleguide.css
Normal file
878
rhodecode/public/css/styleguide.css
Normal file
|
|
@ -0,0 +1,878 @@
|
|||
/* RhodeCode Style Guide — Pure CSS, CSS Grid, No Preprocessors */
|
||||
/* Capability-Driven Presentation: content for everyone, interactivity for those who can handle it */
|
||||
|
||||
/* ========================================================================
|
||||
DESIGN TOKENS
|
||||
======================================================================== */
|
||||
|
||||
:root {
|
||||
/* Colors */
|
||||
--bg: #fafafa;
|
||||
--surface: #ffffff;
|
||||
--surface-alt: #f3f4f6;
|
||||
--border: #e0e0e0;
|
||||
--border-focus: #2563eb;
|
||||
|
||||
--text: #1a1a1a;
|
||||
--text-muted: #6b7280;
|
||||
--text-inverse: #ffffff;
|
||||
|
||||
--accent: #2563eb;
|
||||
--accent-hover: #1d4ed8;
|
||||
--accent-light: #dbeafe;
|
||||
|
||||
--success: #059669;
|
||||
--success-light: #d1fae5;
|
||||
--warning: #d97706;
|
||||
--warning-light: #fef3c7;
|
||||
--danger: #dc2626;
|
||||
--danger-light: #fee2e2;
|
||||
--info: #2563eb;
|
||||
--info-light: #dbeafe;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
--font-mono: 'Berkeley Mono', 'JetBrains Mono', 'Fira Code', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', monospace;
|
||||
--font-size: 16px;
|
||||
--line-height: 1.6;
|
||||
|
||||
/* Spacing scale (4px base) */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 24px;
|
||||
--space-6: 32px;
|
||||
--space-7: 48px;
|
||||
--space-8: 64px;
|
||||
|
||||
/* Borders */
|
||||
--radius-sm: 3px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 10px;
|
||||
--border-width: 1px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Layout */
|
||||
--content-max: 1200px;
|
||||
--sidebar-width: 260px;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
RESET & BASE
|
||||
======================================================================== */
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-height);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body.sg-body {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
LAYOUT — CSS Grid Only, No Flexbox
|
||||
======================================================================== */
|
||||
|
||||
.sg-layout {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-width) 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sg-header {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4) var(--space-6);
|
||||
background: var(--text);
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.sg-header h1 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.sg-header .sg-version {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.sg-sidebar {
|
||||
padding: var(--space-5);
|
||||
background: var(--surface);
|
||||
border-right: var(--border-width) solid var(--border);
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sg-sidebar nav a {
|
||||
display: block;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.sg-sidebar nav a:hover {
|
||||
color: var(--text);
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
.sg-sidebar nav a.active {
|
||||
color: var(--accent);
|
||||
background: var(--accent-light);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sg-sidebar h3 {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
padding: var(--space-4) var(--space-3) var(--space-2);
|
||||
}
|
||||
|
||||
.sg-main {
|
||||
padding: var(--space-7);
|
||||
overflow-y: auto;
|
||||
max-width: var(--content-max);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
SECTIONS
|
||||
======================================================================== */
|
||||
|
||||
.sg-section {
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.sg-section > h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: var(--space-2);
|
||||
padding-bottom: var(--space-3);
|
||||
border-bottom: 2px solid var(--text);
|
||||
}
|
||||
|
||||
.sg-section > p.sg-desc {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: var(--space-6);
|
||||
max-width: 65ch;
|
||||
}
|
||||
|
||||
.sg-subsection {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.sg-subsection > h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--space-4);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
COMPONENT SHOWCASE
|
||||
======================================================================== */
|
||||
|
||||
.sg-showcase {
|
||||
padding: var(--space-5);
|
||||
background: var(--surface);
|
||||
border: var(--border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.sg-showcase + .sg-code {
|
||||
margin-top: calc(-1 * var(--space-4));
|
||||
border-top: none;
|
||||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
||||
}
|
||||
|
||||
.sg-code {
|
||||
background: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.7;
|
||||
overflow-x: auto;
|
||||
margin-bottom: var(--space-4);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
COLOR SWATCHES
|
||||
======================================================================== */
|
||||
|
||||
.sg-colors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.sg-swatch {
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
border: var(--border-width) solid var(--border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.sg-swatch-color {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.sg-swatch-info {
|
||||
padding: var(--space-3);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.sg-swatch-info .sg-swatch-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.sg-swatch-info .sg-swatch-value {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
TYPOGRAPHY SPECIMENS
|
||||
======================================================================== */
|
||||
|
||||
.sg-type-scale {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.sg-type-specimen {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 1fr;
|
||||
align-items: baseline;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-3) 0;
|
||||
border-bottom: var(--border-width) solid var(--border);
|
||||
}
|
||||
|
||||
.sg-type-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
BUTTONS
|
||||
======================================================================== */
|
||||
|
||||
.btn {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
border: var(--border-width) solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: var(--text-inverse);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--surface-alt);
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
color: var(--text-inverse);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #b91c1c;
|
||||
border-color: #b91c1c;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--accent-light);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: var(--space-1) var(--space-3);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--space-3) var(--space-5);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.sg-button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, auto));
|
||||
gap: var(--space-3);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
FORMS
|
||||
======================================================================== */
|
||||
|
||||
.form-group {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
background: var(--surface);
|
||||
border: var(--border-width) solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-light);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
ALERTS
|
||||
======================================================================== */
|
||||
|
||||
.alert {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: var(--space-3);
|
||||
align-items: start;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
border: var(--border-width) solid transparent;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: var(--success-light);
|
||||
border-color: var(--success);
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: var(--warning-light);
|
||||
border-color: var(--warning);
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: var(--danger-light);
|
||||
border-color: var(--danger);
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: var(--info-light);
|
||||
border-color: var(--info);
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
/* Capability-driven: dismiss button only visible with JS */
|
||||
.alert .alert-dismiss {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
opacity: 0.6;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.alert .alert-dismiss:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
CARDS / PANELS
|
||||
======================================================================== */
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: var(--border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-bottom: var(--border-width) solid var(--border);
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-top: var(--border-width) solid var(--border);
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
TABLES
|
||||
======================================================================== */
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
border: var(--border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
table.sg-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
table.sg-table th {
|
||||
text-align: left;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--surface-alt);
|
||||
font-weight: 600;
|
||||
font-size: 0.8125rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 2px solid var(--border);
|
||||
}
|
||||
|
||||
table.sg-table td {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: var(--border-width) solid var(--border);
|
||||
}
|
||||
|
||||
table.sg-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
table.sg-table tr:hover td {
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
BADGES / TAGS
|
||||
======================================================================== */
|
||||
|
||||
.badge {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
border-radius: var(--radius-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-accent { background: var(--accent-light); color: #1e40af; }
|
||||
.badge-success { background: var(--success-light); color: #065f46; }
|
||||
.badge-warning { background: var(--warning-light); color: #92400e; }
|
||||
.badge-danger { background: var(--danger-light); color: #991b1b; }
|
||||
.badge-neutral { background: var(--surface-alt); color: var(--text-muted); }
|
||||
|
||||
/* ========================================================================
|
||||
DETAILS / ACCORDION (native HTML, progressive enhancement)
|
||||
======================================================================== */
|
||||
|
||||
details.sg-details {
|
||||
border: var(--border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--space-3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
details.sg-details summary {
|
||||
padding: var(--space-4);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
background: var(--surface);
|
||||
user-select: none;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
details.sg-details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
details.sg-details summary::before {
|
||||
content: "\25B6";
|
||||
display: inline-block;
|
||||
margin-right: var(--space-3);
|
||||
font-size: 0.75rem;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
details.sg-details[open] summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
details.sg-details .sg-details-body {
|
||||
padding: var(--space-4);
|
||||
border-top: var(--border-width) solid var(--border);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
DIALOG (native HTML)
|
||||
======================================================================== */
|
||||
|
||||
dialog.sg-dialog {
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 0;
|
||||
max-width: 480px;
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
dialog.sg-dialog::backdrop {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.sg-dialog-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
padding: var(--space-5);
|
||||
border-bottom: var(--border-width) solid var(--border);
|
||||
}
|
||||
|
||||
.sg-dialog-header h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sg-dialog-body {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.sg-dialog-footer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-top: var(--border-width) solid var(--border);
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
CODE BLOCKS
|
||||
======================================================================== */
|
||||
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875em;
|
||||
background: var(--surface-alt);
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
DIFF PREVIEW
|
||||
======================================================================== */
|
||||
|
||||
.diff-block {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
border: var(--border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diff-header {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--surface-alt);
|
||||
font-weight: 600;
|
||||
border-bottom: var(--border-width) solid var(--border);
|
||||
}
|
||||
|
||||
.diff-line {
|
||||
display: grid;
|
||||
grid-template-columns: 50px 50px 1fr;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.diff-line-num {
|
||||
padding: 0 var(--space-3);
|
||||
color: var(--text-muted);
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
border-right: var(--border-width) solid var(--border);
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
.diff-line-content {
|
||||
padding: 0 var(--space-3);
|
||||
}
|
||||
|
||||
.diff-add { background: #dcfce7; }
|
||||
.diff-add .diff-line-num { background: #bbf7d0; }
|
||||
.diff-del { background: #fee2e2; }
|
||||
.diff-del .diff-line-num { background: #fecaca; }
|
||||
|
||||
/* ========================================================================
|
||||
NAVIGATION
|
||||
======================================================================== */
|
||||
|
||||
.nav-bar {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-3) var(--space-5);
|
||||
background: var(--surface);
|
||||
border-bottom: var(--border-width) solid var(--border);
|
||||
}
|
||||
|
||||
.nav-bar .nav-logo {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-bar .nav-links {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: auto;
|
||||
gap: var(--space-1);
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.nav-bar .nav-links a {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.nav-bar .nav-links a:hover {
|
||||
color: var(--text);
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
.nav-bar .nav-actions {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
REPO LIST ITEM
|
||||
======================================================================== */
|
||||
|
||||
.repo-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: var(--space-4);
|
||||
align-items: center;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-bottom: var(--border-width) solid var(--border);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.repo-item:hover {
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
.repo-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.repo-info h4 {
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.repo-info h4 a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.repo-info h4 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.repo-info p {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
.repo-meta {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: var(--space-4);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
NOSCRIPT — Capability-Driven Presentation
|
||||
======================================================================== */
|
||||
|
||||
noscript .noscript-banner {
|
||||
padding: var(--space-4);
|
||||
background: var(--warning-light);
|
||||
color: #92400e;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
RESPONSIVE — Single column on narrow screens
|
||||
======================================================================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sg-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sg-sidebar {
|
||||
position: static;
|
||||
height: auto;
|
||||
border-right: none;
|
||||
border-bottom: var(--border-width) solid var(--border);
|
||||
}
|
||||
|
||||
.sg-main {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
}
|
||||
205
rhodecode/public/js/custom.js
Normal file
205
rhodecode/public/js/custom.js
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
/* custom.js — Vanilla JS utilities, zero dependencies */
|
||||
/* Replaces jQuery patterns with modern DOM APIs */
|
||||
|
||||
(function(window, document) {
|
||||
'use strict';
|
||||
|
||||
/* =====================================================================
|
||||
DOM UTILITIES
|
||||
===================================================================== */
|
||||
|
||||
/** Query single element */
|
||||
window.$ = function(selector, context) {
|
||||
return (context || document).querySelector(selector);
|
||||
};
|
||||
|
||||
/** Query all elements */
|
||||
window.$$ = function(selector, context) {
|
||||
return Array.from((context || document).querySelectorAll(selector));
|
||||
};
|
||||
|
||||
/* =====================================================================
|
||||
EVENT UTILITIES
|
||||
===================================================================== */
|
||||
|
||||
/** DOMContentLoaded shorthand */
|
||||
window.onReady = function(fn) {
|
||||
if (document.readyState !== 'loading') {
|
||||
fn();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', fn);
|
||||
}
|
||||
};
|
||||
|
||||
/** Event delegation */
|
||||
window.onDelegate = function(parentSelector, eventType, childSelector, handler) {
|
||||
var parent = typeof parentSelector === 'string'
|
||||
? document.querySelector(parentSelector)
|
||||
: parentSelector;
|
||||
if (!parent) return;
|
||||
parent.addEventListener(eventType, function(e) {
|
||||
var target = e.target.closest(childSelector);
|
||||
if (target && parent.contains(target)) {
|
||||
handler.call(target, e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* =====================================================================
|
||||
AJAX — fetch wrapper
|
||||
===================================================================== */
|
||||
|
||||
window.ajax = function(url, options) {
|
||||
options = options || {};
|
||||
var method = options.method || 'GET';
|
||||
var headers = options.headers || {};
|
||||
var body = options.body || null;
|
||||
|
||||
if (body && typeof body === 'object' && !(body instanceof FormData)) {
|
||||
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
|
||||
body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: body,
|
||||
credentials: 'same-origin'
|
||||
}).then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
||||
}
|
||||
var contentType = response.headers.get('content-type') || '';
|
||||
if (contentType.indexOf('application/json') !== -1) {
|
||||
return response.json();
|
||||
}
|
||||
return response.text();
|
||||
});
|
||||
};
|
||||
|
||||
window.ajaxPOST = function(url, data, success, failure) {
|
||||
ajax(url, {
|
||||
method: 'POST',
|
||||
body: data
|
||||
}).then(success).catch(failure);
|
||||
};
|
||||
|
||||
/* =====================================================================
|
||||
CLASS MANIPULATION HELPERS
|
||||
===================================================================== */
|
||||
|
||||
window.show = function(el) {
|
||||
if (typeof el === 'string') el = $(el);
|
||||
if (el) el.style.display = '';
|
||||
};
|
||||
|
||||
window.hide = function(el) {
|
||||
if (typeof el === 'string') el = $(el);
|
||||
if (el) el.style.display = 'none';
|
||||
};
|
||||
|
||||
window.toggle = function(el) {
|
||||
if (typeof el === 'string') el = $(el);
|
||||
if (!el) return;
|
||||
el.style.display = (el.style.display === 'none') ? '' : 'none';
|
||||
};
|
||||
|
||||
/* =====================================================================
|
||||
COOKIE UTILITIES
|
||||
===================================================================== */
|
||||
|
||||
window.setCookie = function(name, value, days) {
|
||||
var expires = '';
|
||||
if (days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
expires = '; expires=' + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/; SameSite=Lax';
|
||||
};
|
||||
|
||||
window.getCookie = function(name) {
|
||||
var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||||
return match ? decodeURIComponent(match[2]) : null;
|
||||
};
|
||||
|
||||
/* =====================================================================
|
||||
DIALOG UTILITIES (native <dialog>)
|
||||
===================================================================== */
|
||||
|
||||
window.openDialog = function(selector) {
|
||||
var dialog = typeof selector === 'string' ? $(selector) : selector;
|
||||
if (dialog && dialog.showModal) {
|
||||
dialog.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.closeDialog = function(selector) {
|
||||
var dialog = typeof selector === 'string' ? $(selector) : selector;
|
||||
if (dialog && dialog.close) {
|
||||
dialog.close();
|
||||
}
|
||||
};
|
||||
|
||||
/* =====================================================================
|
||||
CLIPBOARD
|
||||
===================================================================== */
|
||||
|
||||
window.copyToClipboard = function(text) {
|
||||
if (navigator.clipboard) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
// Fallback for older browsers
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
/* =====================================================================
|
||||
TOOLTIP (pure CSS preferred, JS for dynamic)
|
||||
===================================================================== */
|
||||
|
||||
window.tooltip = function(el, text) {
|
||||
if (typeof el === 'string') el = $(el);
|
||||
if (!el) return;
|
||||
el.setAttribute('title', text);
|
||||
};
|
||||
|
||||
/* =====================================================================
|
||||
CAPABILITY DETECTION
|
||||
===================================================================== */
|
||||
|
||||
// Mark the document as JS-capable
|
||||
document.documentElement.classList.add('js');
|
||||
document.documentElement.classList.remove('no-js');
|
||||
|
||||
/* =====================================================================
|
||||
FLASH MESSAGES — auto-dismiss
|
||||
===================================================================== */
|
||||
|
||||
window.dismissAlert = function(el) {
|
||||
if (typeof el === 'string') el = $(el);
|
||||
if (!el) return;
|
||||
el.style.transition = 'opacity 0.2s';
|
||||
el.style.opacity = '0';
|
||||
setTimeout(function() { el.remove(); }, 200);
|
||||
};
|
||||
|
||||
/* =====================================================================
|
||||
SCROLL UTILITIES
|
||||
===================================================================== */
|
||||
|
||||
window.scrollToElement = function(selector) {
|
||||
var el = typeof selector === 'string' ? $(selector) : selector;
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
|
||||
})(window, document);
|
||||
772
rhodecode/templates/styleguide/index.mako
Normal file
772
rhodecode/templates/styleguide/index.mako
Normal file
|
|
@ -0,0 +1,772 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Style Guide — RhodeCode</title>
|
||||
<link rel="stylesheet" href="${h.asset('css/styleguide.css')}">
|
||||
<noscript>
|
||||
<style>.js-only { display: none !important; }</style>
|
||||
</noscript>
|
||||
</head>
|
||||
<body class="sg-body">
|
||||
|
||||
<div class="sg-layout">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="sg-header">
|
||||
<h1>RhodeCode Style Guide</h1>
|
||||
<span></span>
|
||||
<span class="sg-version">v${c.rhodecode_version}</span>
|
||||
</header>
|
||||
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="sg-sidebar">
|
||||
<nav>
|
||||
<h3>Foundation</h3>
|
||||
<a href="#colors">Colors</a>
|
||||
<a href="#typography">Typography</a>
|
||||
<a href="#spacing">Spacing</a>
|
||||
<a href="#grid">CSS Grid Layout</a>
|
||||
|
||||
<h3>Components</h3>
|
||||
<a href="#buttons">Buttons</a>
|
||||
<a href="#forms">Forms</a>
|
||||
<a href="#alerts">Alerts</a>
|
||||
<a href="#cards">Cards</a>
|
||||
<a href="#tables">Tables</a>
|
||||
<a href="#badges">Badges</a>
|
||||
<a href="#details">Details / Accordion</a>
|
||||
<a href="#dialog">Dialog</a>
|
||||
|
||||
<h3>Patterns</h3>
|
||||
<a href="#navigation">Navigation Bar</a>
|
||||
<a href="#repo-list">Repository List</a>
|
||||
<a href="#diff">Diff View</a>
|
||||
<a href="#code">Code Blocks</a>
|
||||
|
||||
<h3>Principles</h3>
|
||||
<a href="#capability">Capability-Driven</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="sg-main">
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- COLORS -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="colors">
|
||||
<h2>Colors</h2>
|
||||
<p class="sg-desc">
|
||||
All colors are CSS custom properties defined on <code>:root</code>.
|
||||
No preprocessor variables. Change the token, change the world.
|
||||
</p>
|
||||
|
||||
<div class="sg-subsection">
|
||||
<h3>Core</h3>
|
||||
<div class="sg-colors">
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch-color" style="background: var(--bg)"></div>
|
||||
<div class="sg-swatch-info">
|
||||
<div class="sg-swatch-name">--bg</div>
|
||||
<div class="sg-swatch-value">#fafafa</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch-color" style="background: var(--surface)"></div>
|
||||
<div class="sg-swatch-info">
|
||||
<div class="sg-swatch-name">--surface</div>
|
||||
<div class="sg-swatch-value">#ffffff</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch-color" style="background: var(--text)"></div>
|
||||
<div class="sg-swatch-info">
|
||||
<div class="sg-swatch-name">--text</div>
|
||||
<div class="sg-swatch-value">#1a1a1a</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch-color" style="background: var(--text-muted)"></div>
|
||||
<div class="sg-swatch-info">
|
||||
<div class="sg-swatch-name">--text-muted</div>
|
||||
<div class="sg-swatch-value">#6b7280</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch-color" style="background: var(--border)"></div>
|
||||
<div class="sg-swatch-info">
|
||||
<div class="sg-swatch-name">--border</div>
|
||||
<div class="sg-swatch-value">#e0e0e0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch-color" style="background: var(--accent)"></div>
|
||||
<div class="sg-swatch-info">
|
||||
<div class="sg-swatch-name">--accent</div>
|
||||
<div class="sg-swatch-value">#2563eb</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-subsection">
|
||||
<h3>Semantic</h3>
|
||||
<div class="sg-colors">
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch-color" style="background: var(--success)"></div>
|
||||
<div class="sg-swatch-info">
|
||||
<div class="sg-swatch-name">--success</div>
|
||||
<div class="sg-swatch-value">#059669</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch-color" style="background: var(--warning)"></div>
|
||||
<div class="sg-swatch-info">
|
||||
<div class="sg-swatch-name">--warning</div>
|
||||
<div class="sg-swatch-value">#d97706</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch-color" style="background: var(--danger)"></div>
|
||||
<div class="sg-swatch-info">
|
||||
<div class="sg-swatch-name">--danger</div>
|
||||
<div class="sg-swatch-value">#dc2626</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sg-swatch">
|
||||
<div class="sg-swatch-color" style="background: var(--info)"></div>
|
||||
<div class="sg-swatch-info">
|
||||
<div class="sg-swatch-name">--info</div>
|
||||
<div class="sg-swatch-value">#2563eb</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- TYPOGRAPHY -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="typography">
|
||||
<h2>Typography</h2>
|
||||
<p class="sg-desc">
|
||||
System font stack for sans-serif. Modern monospace stack for code.
|
||||
16px base. 1.6 line height. Maximum 65ch measure for body text.
|
||||
</p>
|
||||
|
||||
<div class="sg-showcase">
|
||||
<div class="sg-type-scale">
|
||||
<div class="sg-type-specimen">
|
||||
<span class="sg-type-label">2rem</span>
|
||||
<span style="font-size: 2rem; font-weight: 700; letter-spacing: -0.02em">Page Title</span>
|
||||
</div>
|
||||
<div class="sg-type-specimen">
|
||||
<span class="sg-type-label">1.5rem</span>
|
||||
<span style="font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em">Section Title</span>
|
||||
</div>
|
||||
<div class="sg-type-specimen">
|
||||
<span class="sg-type-label">1.125rem</span>
|
||||
<span style="font-size: 1.125rem; font-weight: 600">Subsection Title</span>
|
||||
</div>
|
||||
<div class="sg-type-specimen">
|
||||
<span class="sg-type-label">1rem</span>
|
||||
<span style="font-size: 1rem">Body text — the quick brown fox jumps over the lazy dog.</span>
|
||||
</div>
|
||||
<div class="sg-type-specimen">
|
||||
<span class="sg-type-label">0.875rem</span>
|
||||
<span style="font-size: 0.875rem">Secondary text, form labels, table cells.</span>
|
||||
</div>
|
||||
<div class="sg-type-specimen">
|
||||
<span class="sg-type-label">0.75rem</span>
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted)">Caption, metadata, timestamps.</span>
|
||||
</div>
|
||||
<div class="sg-type-specimen">
|
||||
<span class="sg-type-label">mono</span>
|
||||
<span style="font-family: var(--font-mono); font-size: 0.875rem">git commit -m "code outlasts authors"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- SPACING -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="spacing">
|
||||
<h2>Spacing</h2>
|
||||
<p class="sg-desc">
|
||||
4px base unit. 8-step scale. Use CSS custom properties for consistency.
|
||||
</p>
|
||||
|
||||
<div class="sg-showcase">
|
||||
<div style="display: grid; gap: var(--space-3);">
|
||||
% for i, size in enumerate(['4px', '8px', '12px', '16px', '24px', '32px', '48px', '64px'], 1):
|
||||
<div style="display: grid; grid-template-columns: 80px 1fr; align-items: center; gap: var(--space-4);">
|
||||
<code style="font-size: 0.75rem;">--space-${i}</code>
|
||||
<div style="height: 16px; width: var(--space-${i}); background: var(--accent); border-radius: 2px;"></div>
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- CSS GRID LAYOUT -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="grid">
|
||||
<h2>CSS Grid Layout</h2>
|
||||
<p class="sg-desc">
|
||||
All layout uses CSS Grid. No flexbox. No floats. Grid handles both
|
||||
macro layout (page structure) and micro layout (component internals).
|
||||
</p>
|
||||
|
||||
<div class="sg-subsection">
|
||||
<h3>12-column grid</h3>
|
||||
<div class="sg-showcase">
|
||||
<div style="display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--space-2);">
|
||||
% for i in range(12):
|
||||
<div style="background: var(--accent-light); padding: var(--space-2); text-align: center; font-size: 0.75rem; border-radius: var(--radius-sm); color: var(--accent);">${i+1}</div>
|
||||
% endfor
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--space-2); margin-top: var(--space-3);">
|
||||
<div style="grid-column: span 8; background: var(--accent); color: var(--text-inverse); padding: var(--space-3); border-radius: var(--radius-sm); font-size: 0.8125rem;">span 8 — main content</div>
|
||||
<div style="grid-column: span 4; background: var(--surface-alt); padding: var(--space-3); border-radius: var(--radius-sm); font-size: 0.8125rem; border: 1px solid var(--border);">span 4 — sidebar</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--space-2); margin-top: var(--space-3);">
|
||||
<div style="grid-column: span 4; background: var(--surface-alt); padding: var(--space-3); border-radius: var(--radius-sm); font-size: 0.8125rem; border: 1px solid var(--border);">span 4</div>
|
||||
<div style="grid-column: span 4; background: var(--surface-alt); padding: var(--space-3); border-radius: var(--radius-sm); font-size: 0.8125rem; border: 1px solid var(--border);">span 4</div>
|
||||
<div style="grid-column: span 4; background: var(--surface-alt); padding: var(--space-3); border-radius: var(--radius-sm); font-size: 0.8125rem; border: 1px solid var(--border);">span 4</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-code"><div style="display: grid; grid-template-columns: repeat(12, 1fr); gap: 8px;">
|
||||
<div style="grid-column: span 8;">Main content</div>
|
||||
<div style="grid-column: span 4;">Sidebar</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- BUTTONS -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="buttons">
|
||||
<h2>Buttons</h2>
|
||||
<p class="sg-desc">
|
||||
Simple, accessible buttons. Keyboard-focusable with visible focus rings.
|
||||
Use <code><button></code> for actions, <code><a></code> for navigation.
|
||||
</p>
|
||||
|
||||
<div class="sg-showcase">
|
||||
<div class="sg-button-grid">
|
||||
<button class="btn btn-primary">Primary</button>
|
||||
<button class="btn btn-secondary">Secondary</button>
|
||||
<button class="btn btn-danger">Danger</button>
|
||||
<button class="btn btn-ghost">Ghost</button>
|
||||
<button class="btn btn-primary" disabled>Disabled</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-subsection">
|
||||
<h3>Sizes</h3>
|
||||
<div class="sg-showcase">
|
||||
<div class="sg-button-grid">
|
||||
<button class="btn btn-primary btn-sm">Small</button>
|
||||
<button class="btn btn-primary">Default</button>
|
||||
<button class="btn btn-primary btn-lg">Large</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-code"><button class="btn btn-primary">Primary</button>
|
||||
<button class="btn btn-secondary">Secondary</button>
|
||||
<button class="btn btn-danger">Danger</button>
|
||||
<button class="btn btn-ghost">Ghost</button></div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- FORMS -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="forms">
|
||||
<h2>Forms</h2>
|
||||
<p class="sg-desc">
|
||||
Native HTML5 form elements. No custom dropdowns unless absolutely necessary.
|
||||
Use <code><datalist></code> for autocomplete. Use <code>required</code>,
|
||||
<code>pattern</code>, and <code>type</code> for validation.
|
||||
</p>
|
||||
|
||||
<div class="sg-showcase">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="sg-repo-name">Repository name</label>
|
||||
<input class="form-input" type="text" id="sg-repo-name" placeholder="my-project" required>
|
||||
<span class="form-hint">Letters, numbers, hyphens, and underscores only.</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="sg-repo-type">Type</label>
|
||||
<select class="form-select" id="sg-repo-type">
|
||||
<option>Git</option>
|
||||
<option>Mercurial</option>
|
||||
<option>Subversion</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="sg-desc">Description</label>
|
||||
<textarea class="form-textarea" id="sg-desc" placeholder="A short description of the repository..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox"> Make this repository private
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-primary">Create Repository</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- ALERTS -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="alerts">
|
||||
<h2>Alerts</h2>
|
||||
<p class="sg-desc">
|
||||
Capability-driven: dismiss buttons are <code>js-only</code>. Without JavaScript,
|
||||
users see the message but not a broken dismiss button.
|
||||
</p>
|
||||
|
||||
<div class="sg-showcase" style="display: grid; gap: var(--space-3);">
|
||||
<div class="alert alert-success">
|
||||
<span>✓</span>
|
||||
<span>Repository <strong>my-project</strong> created successfully.</span>
|
||||
<button class="alert-dismiss js-only" onclick="dismissAlert(this.parentElement)">×</button>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<span>⚠</span>
|
||||
<span>Your session will expire in 5 minutes.</span>
|
||||
<button class="alert-dismiss js-only" onclick="dismissAlert(this.parentElement)">×</button>
|
||||
</div>
|
||||
<div class="alert alert-danger">
|
||||
<span>✗</span>
|
||||
<span>Failed to merge pull request: conflicts detected.</span>
|
||||
<button class="alert-dismiss js-only" onclick="dismissAlert(this.parentElement)">×</button>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<span>ℹ</span>
|
||||
<span>A new version of RhodeCode is available.</span>
|
||||
<button class="alert-dismiss js-only" onclick="dismissAlert(this.parentElement)">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- CARDS -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="cards">
|
||||
<h2>Cards</h2>
|
||||
<p class="sg-desc">
|
||||
Content containers with optional header and footer. Use CSS Grid for card layouts.
|
||||
</p>
|
||||
|
||||
<div class="card-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">Pull Request #142</div>
|
||||
<div class="card-body">
|
||||
<p style="font-size: 0.875rem;">Fix session handling for cookie-based authentication. Remove beaker dependency entirely.</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<span class="badge badge-success">Approved</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">Pull Request #143</div>
|
||||
<div class="card-body">
|
||||
<p style="font-size: 0.875rem;">Modernize UI: remove jQuery, switch to CSS Grid, add living style guide endpoint.</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<span class="badge badge-warning">Review Required</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">Pull Request #144</div>
|
||||
<div class="card-body">
|
||||
<p style="font-size: 0.875rem;">Add SQLite3 support for single-instance deployments on permacomputer hardware.</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<span class="badge badge-accent">Draft</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- TABLES -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="tables">
|
||||
<h2>Tables</h2>
|
||||
<p class="sg-desc">
|
||||
Native HTML tables with minimal styling. Wrapped for horizontal scroll on narrow screens.
|
||||
</p>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="sg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Commit</th>
|
||||
<th>Message</th>
|
||||
<th>Author</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>4f73837</code></td>
|
||||
<td>Remove beaker dependency, switch to SignedCookieSessionFactory</td>
|
||||
<td>fox</td>
|
||||
<td>2 hours ago</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ceb23e4</code></td>
|
||||
<td>release(version-bump): 5.11.3 → 5.11.4</td>
|
||||
<td>andverb</td>
|
||||
<td>1 day ago</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>93d4bc5</code></td>
|
||||
<td>Cleanup, better handling for multiline comments</td>
|
||||
<td>andverb</td>
|
||||
<td>3 days ago</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- BADGES -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="badges">
|
||||
<h2>Badges</h2>
|
||||
|
||||
<div class="sg-showcase">
|
||||
<div style="display: grid; grid-auto-flow: column; gap: var(--space-3); justify-content: start;">
|
||||
<span class="badge badge-accent">Feature</span>
|
||||
<span class="badge badge-success">Merged</span>
|
||||
<span class="badge badge-warning">Pending</span>
|
||||
<span class="badge badge-danger">Rejected</span>
|
||||
<span class="badge badge-neutral">Draft</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- DETAILS / ACCORDION -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="details">
|
||||
<h2>Details / Accordion</h2>
|
||||
<p class="sg-desc">
|
||||
Native <code><details></code> element. Works without JavaScript.
|
||||
Progressive enhancement at its finest.
|
||||
</p>
|
||||
|
||||
<details class="sg-details">
|
||||
<summary>Repository Settings</summary>
|
||||
<div class="sg-details-body">
|
||||
<p style="font-size: 0.875rem;">Configure repository name, description, landing page, and permissions. All changes take effect immediately.</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="sg-details">
|
||||
<summary>VCS Configuration</summary>
|
||||
<div class="sg-details-body">
|
||||
<p style="font-size: 0.875rem;">Configure hooks, large file storage, and protocol settings for this repository.</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="sg-details" open>
|
||||
<summary>Danger Zone</summary>
|
||||
<div class="sg-details-body">
|
||||
<p style="font-size: 0.875rem; margin-bottom: var(--space-4);">Destructive actions that cannot be undone. Proceed with caution.</p>
|
||||
<button class="btn btn-danger btn-sm">Delete Repository</button>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- DIALOG -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="dialog">
|
||||
<h2>Dialog</h2>
|
||||
<p class="sg-desc">
|
||||
Native <code><dialog></code> element with <code>showModal()</code>.
|
||||
Accessible by default. Backdrop handled by CSS.
|
||||
The trigger button is <code>js-only</code> — no broken button without JS.
|
||||
</p>
|
||||
|
||||
<div class="sg-showcase">
|
||||
<button class="btn btn-primary js-only" onclick="openDialog('#sg-demo-dialog')">Open Dialog</button>
|
||||
<noscript><p style="font-size: 0.875rem; color: var(--text-muted);">Dialog requires JavaScript.</p></noscript>
|
||||
</div>
|
||||
|
||||
<dialog id="sg-demo-dialog" class="sg-dialog">
|
||||
<div class="sg-dialog-header">
|
||||
<h3>Confirm Merge</h3>
|
||||
<button class="btn btn-ghost btn-sm" onclick="closeDialog('#sg-demo-dialog')">×</button>
|
||||
</div>
|
||||
<div class="sg-dialog-body">
|
||||
<p style="font-size: 0.875rem;">
|
||||
You are about to merge pull request <strong>#142</strong> into <code>main</code>.
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
<div class="sg-dialog-footer">
|
||||
<span></span>
|
||||
<button class="btn btn-secondary" onclick="closeDialog('#sg-demo-dialog')">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="closeDialog('#sg-demo-dialog')">Merge</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- NAVIGATION BAR -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="navigation">
|
||||
<h2>Navigation Bar</h2>
|
||||
<p class="sg-desc">
|
||||
Top navigation using CSS Grid. Three-column layout: logo, links, actions.
|
||||
</p>
|
||||
|
||||
<div class="sg-showcase" style="padding: 0;">
|
||||
<nav class="nav-bar">
|
||||
<a class="nav-logo" href="#">RhodeCode</a>
|
||||
<div class="nav-links">
|
||||
<a href="#">Repositories</a>
|
||||
<a href="#">Groups</a>
|
||||
<a href="#">Admin</a>
|
||||
</div>
|
||||
<div class="nav-actions">
|
||||
<button class="btn btn-primary btn-sm">New Repo</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- REPOSITORY LIST -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="repo-list">
|
||||
<h2>Repository List</h2>
|
||||
<p class="sg-desc">
|
||||
Repository listing with icon, info, and metadata. CSS Grid three-column layout.
|
||||
</p>
|
||||
|
||||
<div class="sg-showcase" style="padding: 0;">
|
||||
<div class="card">
|
||||
<div class="repo-item">
|
||||
<div class="repo-icon">G</div>
|
||||
<div class="repo-info">
|
||||
<h4><a href="#">rhodecode-enterprise-ce</a></h4>
|
||||
<p>Enterprise Source Code Management Platform</p>
|
||||
</div>
|
||||
<div class="repo-meta">
|
||||
<span>★ 42</span>
|
||||
<span>⋔ 7</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-item">
|
||||
<div class="repo-icon" style="background: var(--success-light); color: var(--success);">M</div>
|
||||
<div class="repo-info">
|
||||
<h4><a href="#">make_post_sell</a></h4>
|
||||
<p>Commission-free digital and physical sales</p>
|
||||
</div>
|
||||
<div class="repo-meta">
|
||||
<span>★ 18</span>
|
||||
<span>⋔ 3</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-item">
|
||||
<div class="repo-icon" style="background: var(--warning-light); color: var(--warning);">H</div>
|
||||
<div class="repo-info">
|
||||
<h4><a href="#">remarkbox</a></h4>
|
||||
<p>Hosted comment system, embeds in pages</p>
|
||||
</div>
|
||||
<div class="repo-meta">
|
||||
<span>★ 31</span>
|
||||
<span>⋔ 5</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- DIFF VIEW -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="diff">
|
||||
<h2>Diff View</h2>
|
||||
<p class="sg-desc">
|
||||
Side-by-side line numbers with CSS Grid. Monospace font. Addition and deletion colors.
|
||||
</p>
|
||||
|
||||
<div class="diff-block">
|
||||
<div class="diff-header">rhodecode/lib/rc_beaker.py</div>
|
||||
<div class="diff-line diff-del">
|
||||
<span class="diff-line-num">8</span>
|
||||
<span class="diff-line-num"></span>
|
||||
<span class="diff-line-content">-from beaker import cache</span>
|
||||
</div>
|
||||
<div class="diff-line diff-del">
|
||||
<span class="diff-line-num">9</span>
|
||||
<span class="diff-line-num"></span>
|
||||
<span class="diff-line-content">-from beaker.session import SessionObject, Session</span>
|
||||
</div>
|
||||
<div class="diff-line diff-add">
|
||||
<span class="diff-line-num"></span>
|
||||
<span class="diff-line-num">8</span>
|
||||
<span class="diff-line-content">+from pyramid.session import SignedCookieSessionFactory</span>
|
||||
</div>
|
||||
<div class="diff-line diff-add">
|
||||
<span class="diff-line-num"></span>
|
||||
<span class="diff-line-num">9</span>
|
||||
<span class="diff-line-content">+from pyramid.settings import asbool</span>
|
||||
</div>
|
||||
<div class="diff-line">
|
||||
<span class="diff-line-num">10</span>
|
||||
<span class="diff-line-num">10</span>
|
||||
<span class="diff-line-content"> </span>
|
||||
</div>
|
||||
<div class="diff-line">
|
||||
<span class="diff-line-num">11</span>
|
||||
<span class="diff-line-num">11</span>
|
||||
<span class="diff-line-content"> </span>
|
||||
</div>
|
||||
<div class="diff-line diff-del">
|
||||
<span class="diff-line-num">12</span>
|
||||
<span class="diff-line-num"></span>
|
||||
<span class="diff-line-content">-class CustomSession(Session):</span>
|
||||
</div>
|
||||
<div class="diff-line diff-del">
|
||||
<span class="diff-line-num">13</span>
|
||||
<span class="diff-line-num"></span>
|
||||
<span class="diff-line-content">- pass</span>
|
||||
</div>
|
||||
<div class="diff-line diff-add">
|
||||
<span class="diff-line-num"></span>
|
||||
<span class="diff-line-num">12</span>
|
||||
<span class="diff-line-content">+def session_factory_from_settings(settings):</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- CODE BLOCKS -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="code">
|
||||
<h2>Code Blocks</h2>
|
||||
<p class="sg-desc">
|
||||
Inline <code>code</code> and block-level pre/code. Monospace font stack.
|
||||
</p>
|
||||
|
||||
<div class="sg-code">def session_factory_from_settings(settings):
|
||||
"""Return a Pyramid session factory using signed cookie session settings."""
|
||||
prefixes = ("session.", "beaker.session.")
|
||||
options = {}
|
||||
for k, v in settings.items():
|
||||
for prefix in prefixes:
|
||||
if k.startswith(prefix):
|
||||
options[k[len(prefix):]] = v
|
||||
return SignedCookieSessionFactory(**factory_kwargs)</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- CAPABILITY-DRIVEN PRESENTATION -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="sg-section" id="capability">
|
||||
<h2>Capability-Driven Presentation</h2>
|
||||
<p class="sg-desc">
|
||||
A web page does not need to look the same on every browser or device.
|
||||
Capability drives presentation. Deliver identical content regardless of
|
||||
viewer capabilities. Use the viewer's capabilities to gracefully enhance
|
||||
the presentation.
|
||||
</p>
|
||||
|
||||
<div class="sg-subsection">
|
||||
<h3>The <code>js-only</code> Pattern</h3>
|
||||
<p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: var(--space-4); max-width: 65ch;">
|
||||
Elements with <code>class="js-only"</code> are hidden when JavaScript is
|
||||
unavailable. This prevents presenting broken interactive elements to users
|
||||
whose browser cannot execute them.
|
||||
</p>
|
||||
|
||||
<div class="sg-code"><noscript>
|
||||
<style>.js-only { display: none !important; }</style>
|
||||
</noscript>
|
||||
|
||||
<!-- The dismiss button only shows when JS can handle it -->
|
||||
<div class="alert alert-success">
|
||||
<span>Repository created.</span>
|
||||
<button class="alert-dismiss js-only">&times;</button>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<div class="sg-subsection">
|
||||
<h3>The Rules</h3>
|
||||
<div class="sg-showcase">
|
||||
<ol style="font-size: 0.875rem; padding-left: var(--space-5); display: grid; gap: var(--space-3);">
|
||||
<li>Maintain a single canonical URI for all users.</li>
|
||||
<li>Deliver identical content regardless of viewer capabilities.</li>
|
||||
<li>Use the viewer's capabilities to gracefully enhance the presentation.</li>
|
||||
<li>Never present a broken button to the user.</li>
|
||||
<li>CSS Grid only. No flexbox. No floats.</li>
|
||||
<li>No jQuery. No CSS preprocessors. No framework bloat.</li>
|
||||
<li>Native HTML5 elements first: <code><details></code>, <code><dialog></code>, <code><datalist></code>.</li>
|
||||
<li>Code outlasts authors. Infrastructure outlasts builders.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="${h.asset('js/custom.js')}"></script>
|
||||
|
||||
<script>
|
||||
// Sidebar active state on scroll
|
||||
onReady(function() {
|
||||
var links = $$('.sg-sidebar nav a');
|
||||
var sections = $$('.sg-section[id]');
|
||||
|
||||
// Smooth scroll for sidebar links
|
||||
links.forEach(function(link) {
|
||||
link.addEventListener('click', function(e) {
|
||||
var href = this.getAttribute('href');
|
||||
if (href && href.startsWith('#')) {
|
||||
e.preventDefault();
|
||||
scrollToElement(href);
|
||||
history.replaceState(null, '', href);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight active section on scroll
|
||||
var main = $('.sg-main');
|
||||
if (main) {
|
||||
main.addEventListener('scroll', function() {
|
||||
var scrollTop = main.scrollTop;
|
||||
var current = '';
|
||||
sections.forEach(function(section) {
|
||||
if (section.offsetTop - 100 <= scrollTop) {
|
||||
current = section.id;
|
||||
}
|
||||
});
|
||||
links.forEach(function(link) {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue