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:
russell@unturf.com 2026-02-19 16:03:21 -05:00
parent 4f73837822
commit 340e25aa71
7 changed files with 1979 additions and 0 deletions

View 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,
)

View 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,
)

View file

@ -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")

View 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);
}
}

View 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);

View 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 &mdash; 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 &mdash; 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 &mdash; 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">&lt;div style="display: grid; grid-template-columns: repeat(12, 1fr); gap: 8px;"&gt;
&lt;div style="grid-column: span 8;"&gt;Main content&lt;/div&gt;
&lt;div style="grid-column: span 4;"&gt;Sidebar&lt;/div&gt;
&lt;/div&gt;</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>&lt;button&gt;</code> for actions, <code>&lt;a&gt;</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">&lt;button class="btn btn-primary"&gt;Primary&lt;/button&gt;
&lt;button class="btn btn-secondary"&gt;Secondary&lt;/button&gt;
&lt;button class="btn btn-danger"&gt;Danger&lt;/button&gt;
&lt;button class="btn btn-ghost"&gt;Ghost&lt;/button&gt;</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>&lt;datalist&gt;</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>&#10003;</span>
<span>Repository <strong>my-project</strong> created successfully.</span>
<button class="alert-dismiss js-only" onclick="dismissAlert(this.parentElement)">&times;</button>
</div>
<div class="alert alert-warning">
<span>&#9888;</span>
<span>Your session will expire in 5 minutes.</span>
<button class="alert-dismiss js-only" onclick="dismissAlert(this.parentElement)">&times;</button>
</div>
<div class="alert alert-danger">
<span>&#10007;</span>
<span>Failed to merge pull request: conflicts detected.</span>
<button class="alert-dismiss js-only" onclick="dismissAlert(this.parentElement)">&times;</button>
</div>
<div class="alert alert-info">
<span>&#8505;</span>
<span>A new version of RhodeCode is available.</span>
<button class="alert-dismiss js-only" onclick="dismissAlert(this.parentElement)">&times;</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 &rarr; 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>&lt;details&gt;</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>&lt;dialog&gt;</code> element with <code>showModal()</code>.
Accessible by default. Backdrop handled by CSS.
The trigger button is <code>js-only</code> &mdash; 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')">&times;</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>&#9733; 42</span>
<span>&#8916; 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>&#9733; 18</span>
<span>&#8916; 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>&#9733; 31</span>
<span>&#8916; 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">&lt;noscript&gt;
&lt;style&gt;.js-only { display: none !important; }&lt;/style&gt;
&lt;/noscript&gt;
&lt;!-- The dismiss button only shows when JS can handle it --&gt;
&lt;div class="alert alert-success"&gt;
&lt;span&gt;Repository created.&lt;/span&gt;
&lt;button class="alert-dismiss js-only"&gt;&amp;times;&lt;/button&gt;
&lt;/div&gt;</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>&lt;details&gt;</code>, <code>&lt;dialog&gt;</code>, <code>&lt;datalist&gt;</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>