mockups/search.html
Rama main · réplica de lectura del repositorio original.
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search — Cloudzilla</title>
<link rel="stylesheet" href="/assets/tailwind.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500&family=Geist:wght@400;500;600&display=swap" rel="stylesheet" />
</head>
<body class="min-h-screen flex flex-col antialiased font-sans">
<a href="#main-content" class="skip-link">Skip to main content</a>
<div id="cz-header"></div>
<main id="main-content" tabindex="-1" class="max-w-[1400px] mx-auto px-6 pt-6 pb-20 flex-1 w-full">
<div class="space-y-6">
<div class="rounded-lg border border-border bg-card text-card-foreground">
<div class="p-5">
<h1 class="text-2xl font-bold mb-4">Search</h1>
<form action="search.html" method="GET" class="flex gap-2" id="search-form">
<label for="search-query" class="sr-only">Search query</label>
<div class="relative flex-1">
<input id="search-query" type="search" name="q" placeholder="Search repos, issues, PRs, users..." autocomplete="off" role="combobox" aria-autocomplete="list" aria-haspopup="listbox" aria-controls="search-suggest" aria-expanded="false" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 pr-9" />
<button type="button" id="search-clear" class="hidden absolute right-2 top-1/2 -translate-y-1/2 h-6 w-6 grid place-items-center rounded-full text-muted-foreground hover:text-foreground hover:bg-accent" aria-label="Clear search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
<div id="search-suggest" class="hidden absolute inset-x-0 top-full mt-1 z-50 overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-lg"></div>
</div>
<button type="submit" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors disabled:opacity-50 disabled:pointer-events-none bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 text-sm">Search</button>
</form>
</div>
</div>
<div id="search-results" class="space-y-6"></div>
</div>
</main>
<div id="cz-footer"></div>
<script src="shell.js"></script>
<script>
(function () {
var TYPES = [['all', 'All'], ['repos', 'Repositories'], ['orgs', 'Organizations'], ['issues', 'Issues'], ['pulls', 'Pull requests'], ['users', 'Users']];
var GROUPS = { repos: 'Repositories', orgs: 'Organizations', issues: 'Issues', pulls: 'Pull requests', users: 'Users' };
var DATA = {
repos: [
{ owner: 'acme-corp', name: 'oauth-proxy', desc: 'Reverse proxy that fronts internal apps with OAuth 2.0 and OIDC login', priv: false, time: '2d ago', lang: 'Go', dot: 'bg-sky-400' },
{ owner: 'mkappworks', name: 'cloudzilla', desc: 'Self-hosted git forge with Google OAuth and SSO login', priv: false, time: '3h ago', lang: 'Go', dot: 'bg-sky-400' },
{ owner: 'stitch-labs', name: 'oauth-demo', desc: 'Sample app for the authorization-code flow with PKCE', priv: true, time: '3w ago', lang: 'TypeScript', dot: 'bg-blue-500' }
],
orgs: [
{ name: 'oauth-working-group', sub: 'OAuth 2.0 tooling and reference clients' }
],
issues: [
{ repo: 'mkappworks/cloudzilla', n: 214, title: 'Google OAuth callback loses the invite token', author: 'daisy', state: 'open', time: '1d ago', comments: 4, labels: [['bug', '#ef4444']] },
{ repo: 'acme-corp/oauth-proxy', n: 57, title: 'Refresh the OAuth access token before it expires', author: 'kiyo', state: 'open', time: '5d ago', comments: 2, labels: [['enhancement', '#3b82f6']] },
{ repo: 'mkappworks/cloudzilla', n: 188, title: 'Document the OAuth redirect URI for self-hosters', author: 'rhea', state: 'closed', time: '3w ago', comments: 6, labels: [['docs', '#22c55e']] }
],
pulls: [
{ owner: 'mkappworks', repo: 'cloudzilla', n: 231, title: 'feat(auth): add OAuth device-flow login for the CLI', author: 'malith', state: 'open', time: '4h ago' },
{ owner: 'acme-corp', repo: 'oauth-proxy', n: 61, title: 'fix: reject OAuth state mismatches', author: 'kiyo', state: 'merged', time: '2w ago' },
{ owner: 'stitch-labs', repo: 'oauth-demo', n: 12, title: 'wip: OAuth PKCE example', author: 'rhea', state: 'draft', time: '1mo ago' }
],
users: [
{ name: 'oauth-bot', sub: 'Automated token rotation for OAuth clients' },
{ name: 'oauthfan', sub: 'Identity and access nerd' }
]
};
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function initials(n) { var p = n.trim().split(/\s+/); return (p.length > 1 ? p[0][0] + p[p.length - 1][0] : p[0][0]).toUpperCase(); }
var query = '';
var type = 'all';
// Marks words that start with a query token, like components.Highlight.
function hl(text, q) {
var toks = q.split(/\s+/).filter(Boolean).map(function (t) { return t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); });
if (!toks.length) return esc(text);
var re = new RegExp('(^|[^A-Za-z0-9])(' + toks.join('|') + ')', 'gi');
var out = '', last = 0, m;
while ((m = re.exec(text))) {
var start = m.index + m[1].length;
out += esc(text.slice(last, start)) + '<mark class="bg-warning/25 text-inherit rounded-sm">' + esc(m[2]) + '</mark>';
last = start + m[2].length;
}
return out + esc(text.slice(last));
}
function matches(q, s) {
var toks = q.toLowerCase().split(/\s+/).filter(Boolean);
s = s.toLowerCase();
return toks.every(function (t) { return s.indexOf(t) >= 0; });
}
function search(q) {
return {
repos: DATA.repos.filter(function (r) { return matches(q, r.owner + '/' + r.name + ' ' + r.desc); }),
orgs: DATA.orgs.filter(function (o) { return matches(q, o.name + ' ' + o.sub); }),
issues: DATA.issues.filter(function (i) { return matches(q, i.title + ' ' + i.repo); }),
pulls: DATA.pulls.filter(function (p) { return matches(q, p.title + ' ' + p.owner + '/' + p.repo); }),
users: DATA.users.filter(function (u) { return matches(q, u.name + ' ' + u.sub); })
};
}
var ROW_LINK = 'group relative block px-4 py-3 transition-[padding] hover:bg-accent hover:pl-[18px] before:content-[\'\'] before:absolute before:left-0 before:top-2 before:bottom-2 before:w-0.5 before:rounded-full before:bg-foreground before:opacity-0 before:transition-opacity hover:before:opacity-100';
var SEP = '<span class="opacity-50" aria-hidden="true">·</span>';
var COMMENT_ICON = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z"></path></svg>';
var BADGE = 'inline-flex items-center gap-1.5 h-[22px] px-2 rounded-full border text-[11px] font-medium ';
var BADGES = {
open: [BADGE + 'bg-success/15 text-success border-success/30', 'Open'],
closed: [BADGE + 'bg-destructive/15 text-destructive border-destructive/30', 'Closed'],
merged: [BADGE + 'bg-[hsl(270_70%_50%/0.15)] text-[hsl(270_70%_70%)] border-[hsl(270_70%_50%/0.30)]', 'Merged'],
draft: [BADGE + 'bg-muted text-muted-foreground border-transparent', 'Draft']
};
function badge(state) { return '<span class="' + BADGES[state][0] + '">' + BADGES[state][1] + '</span>'; }
var CHIP_STYLES = {
'#22c55e': '--label-bg:#22c55e;--label-fg:#1f2328;--label-border:#1ead53;--label-dark-bg:#22c55e2e;--label-dark-border:#22c55e66;--label-dark-fg:#22c55e',
'#3b82f6': '--label-bg:#4588f6;--label-fg:#1f2328;--label-border:#3d78d8;--label-dark-bg:#3b82f62e;--label-dark-border:#3b82f666;--label-dark-fg:#5895f7',
'#ef4444': '--label-bg:#f15757;--label-fg:#1f2328;--label-border:#d44d4d;--label-dark-bg:#ef44442e;--label-dark-border:#ef444466;--label-dark-fg:#f26969'
};
function chip(c) {
return '<span class="inline-block max-w-full truncate align-middle rounded-full border px-2 py-px text-[11px] leading-4 font-medium border-(color:--label-border) bg-(color:--label-bg) text-(color:--label-fg) dark:border-(color:--label-dark-border) dark:bg-(color:--label-dark-bg) dark:text-(color:--label-dark-fg)" style="' + CHIP_STYLES[c[1]] + '">' + esc(c[0]) + '</span>';
}
var ISSUE_ICON = {
open: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="shrink-0 mt-0.5 text-success" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><circle cx="12" cy="12" r="3"></circle></svg>',
closed: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="shrink-0 mt-0.5 text-destructive" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M9 9l6 6M15 9l-6 6"></path></svg>'
};
var PR_ICON = {
merged: '<svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor" class="shrink-0 mt-0.5 text-[hsl(270_70%_70%)]" aria-hidden="true"><path d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>',
draft: '<svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor" class="shrink-0 mt-0.5 text-muted-foreground" aria-hidden="true"><path d="M1.5 3.25a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm0 9.5a.75.75 0 100 1.5.75.75 0 000-1.5zm6.5-9.25a2.25 2.25 0 113 2.122V10.5a2.25 2.25 0 11-1.5 0V4.622a2.25 2.25 0 01-1.5-2.122zm2.25-.75a.75.75 0 100 1.5.75.75 0 000-1.5zm0 9.5a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>',
open: '<svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor" class="shrink-0 mt-0.5 text-success" aria-hidden="true"><path d="M1.5 3.25a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zm5.677-.177L9.573.677A.25.25 0 0110 .854V2.5h1A2.5 2.5 0 0113.5 5v5.628a2.251 2.251 0 11-1.5 0V5a1 1 0 00-1-1h-1v1.646a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm0 9.5a.75.75 0 100 1.5.75.75 0 000-1.5zm8.25.75a.75.75 0 101.5 0 .75.75 0 00-1.5 0z"></path></svg>'
};
function repoRow(r, q) {
return '<li><a href="repo.html" class="' + ROW_LINK + '">' +
'<div class="flex items-start gap-3 mb-1.5">' +
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" class="shrink-0 mt-0.5 text-muted-foreground" aria-hidden="true" focusable="false"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"></path><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"></path></svg>' +
'<div class="min-w-0 flex-1 flex items-center gap-2 flex-wrap"><p class="text-[13.5px] font-medium leading-snug group-hover:underline">' + hl(r.owner + '/' + r.name, q) + '</p>' +
(r.priv
? '<span class="inline-flex items-center shrink-0 h-[20px] px-2 rounded-full border text-[10px] font-medium bg-warning/15 text-warning border-warning/30">Private</span>'
: '<span class="inline-flex items-center shrink-0 h-[20px] px-2 rounded-full border text-[10px] font-medium bg-success/15 text-success border-success/30">Public</span>') +
'</div></div>' +
'<p class="pl-[27px] mb-1.5 text-[12.5px] text-muted-foreground truncate">' + esc(r.desc) + '</p>' +
'<div class="flex items-center justify-between gap-3 pl-[27px]"><div class="flex items-center gap-1.5 text-[12px] text-muted-foreground min-w-0 flex-wrap">' +
'<span class="inline-flex items-center gap-1"><span class="w-2 h-2 rounded-full ' + r.dot + '" aria-hidden="true"></span>' + esc(r.lang) + '</span>' + SEP +
'<time>Updated ' + esc(r.time) + '</time></div></div></a></li>';
}
function personRow(p, q, href) {
return '<li class="flex items-center gap-4 px-4 py-3.5">' +
'<span class="inline-grid place-items-center rounded-full bg-muted border border-border font-medium text-muted-foreground h-10 w-10 text-sm" role="img" aria-label="' + esc(p.name) + '">' + esc(initials(p.name)) + '</span>' +
'<div class="min-w-0 flex-1"><div class="flex flex-wrap items-center gap-2"><a href="' + href + '" class="text-sm font-medium hover:underline">' + hl(p.name, q) + '</a></div>' +
'<p class="text-xs text-muted-foreground mt-0.5 truncate">' + esc(p.sub) + '</p></div></li>';
}
function issueRow(i, q) {
return '<li><a href="issue_detail.html" class="' + ROW_LINK + '">' +
'<div class="flex items-start gap-3 mb-1.5">' + ISSUE_ICON[i.state] +
'<div class="min-w-0 flex-1 flex items-center gap-2 flex-wrap"><span class="font-mono text-[12px] text-muted-foreground">' + esc(i.repo) + '</span>' + SEP +
'<p class="text-[13.5px] font-medium leading-snug group-hover:underline">' + hl(i.title, q) + '</p>' + badge(i.state) + '</div></div>' +
'<div class="flex items-center justify-between gap-3 pl-[27px]"><div class="flex items-center gap-1.5 text-[12px] text-muted-foreground min-w-0 flex-wrap">' +
'<span class="font-mono">#' + i.n + '</span>' + SEP +
'<span class="inline-flex items-center justify-center h-4 w-4 rounded-full border border-border bg-muted text-[8px] font-medium uppercase text-muted-foreground select-none" aria-hidden="true">' + esc(initials(i.author)) + '</span>' +
'<span>' + esc(i.author) + '</span>' + SEP + '<time>' + esc(i.time) + '</time>' +
(i.labels.length ? '<span class="opacity-50 mx-0.5" aria-hidden="true">·</span>' + i.labels.map(chip).join('') : '') + '</div>' +
'<div class="flex items-center gap-3 text-[12px] text-muted-foreground shrink-0">' +
(i.comments ? '<span class="inline-flex items-center gap-1" title="' + i.comments + (i.comments === 1 ? ' comment' : ' comments') + '">' + COMMENT_ICON + i.comments + '</span>' : '') +
'</div></div></a></li>';
}
function prRow(p, q) {
return '<li class="border-b border-border last:border-0"><a href="pr.html" class="' + ROW_LINK + '">' +
'<div class="flex items-start gap-3 mb-1.5">' + PR_ICON[p.state] +
'<div class="min-w-0 flex-1 flex items-center gap-2 flex-wrap"><p class="text-[13.5px] font-medium leading-snug group-hover:underline">' + hl(p.title, q) + '</p>' + badge(p.state) + '</div></div>' +
'<div class="flex items-center justify-between gap-3 pl-[27px]"><div class="flex items-center gap-1.5 text-xs text-muted-foreground min-w-0 flex-wrap">' +
'<span>' + esc(p.owner + '/' + p.repo) + '</span><span class="font-mono">#' + p.n + '</span>' + SEP +
'<span class="inline-grid place-items-center h-4 w-4 rounded-full bg-muted border border-border text-[8px] font-medium" aria-hidden="true">' + esc(initials(p.author)) + '</span>' +
'<span>' + esc(p.author) + '</span>' + SEP + '<span>' + esc(p.time) + '</span></div>' +
'<div class="flex items-center gap-3 text-xs shrink-0"></div></div></a></li>';
}
function group(key, rows) {
return '<section class="rounded-md border border-border bg-card overflow-hidden">' +
(type === 'all' ? '<h2 class="px-6 py-3 text-xs font-semibold text-muted-foreground uppercase tracking-wide bg-muted">' + GROUPS[key] + '</h2>' : '') +
'<ul class="divide-y divide-border">' + rows + '</ul></section>';
}
function tabs(res) {
var cls = 'px-3 py-2.5 border-b -mb-px inline-flex items-center gap-1.5 ';
return '<nav aria-label="Section navigation" class="flex items-center gap-1 border-b border-border text-sm">' + TYPES.map(function (t) {
var n = t[0] === 'all' ? 0 : res[t[0]].length;
var active = t[0] === type;
return '<a href="search.html?q=' + encodeURIComponent(query) + '&type=' + t[0] + '" data-type="' + t[0] + '"' + (active ? ' aria-current="page"' : '') +
' class="' + cls + (active ? 'border-foreground text-foreground font-medium' : 'border-transparent text-muted-foreground hover:text-foreground') + '">' + t[1] +
(n ? '<span class="ml-0.5 text-[11px] text-muted-foreground font-mono" aria-label="' + n + (n === 1 ? ' item' : ' items') + '">' + n + '</span>' : '') + '</a>';
}).join('') + '</nav>';
}
var EMPTY = 'bg-background border border-border rounded-lg p-8 text-center';
function render() {
var out = document.getElementById('search-results');
document.title = (query ? 'Search: ' + query : 'Search') + ' — Cloudzilla';
if (!query) {
out.innerHTML = '<div class="' + EMPTY + '"><p class="text-muted-foreground">Enter a search query above to get started.</p></div>';
return;
}
var res = search(query);
var shows = function (k) { return type === 'all' || type === k; };
var html = tabs(res);
if (shows('repos') && res.repos.length) html += group('repos', res.repos.map(function (r) { return repoRow(r, query); }).join(''));
if (shows('orgs') && res.orgs.length) html += group('orgs', res.orgs.map(function (o) { return personRow(o, query, 'org_profile.html'); }).join(''));
if (shows('issues') && res.issues.length) html += group('issues', res.issues.map(function (i) { return issueRow(i, query); }).join(''));
if (shows('pulls') && res.pulls.length) html += group('pulls', res.pulls.map(function (p) { return prRow(p, query); }).join(''));
if (shows('users') && res.users.length) html += group('users', res.users.map(function (u) { return personRow(u, query, 'profile.html'); }).join(''));
// The real page only shows this when every group is empty, not just the selected tab's.
if (!res.repos.length && !res.orgs.length && !res.issues.length && !res.pulls.length && !res.users.length) {
html += '<div class="' + EMPTY + '"><p class="text-muted-foreground">No results found for ' + esc(query) + '.</p>' +
'<p class="text-sm text-muted-foreground mt-2">Try different keywords or broaden your search.</p></div>';
}
out.innerHTML = html;
}
var input = document.getElementById('search-query');
var clear = document.getElementById('search-clear');
var suggest = window.czAttachSuggest(input, document.getElementById('search-suggest'));
function syncClear() { clear.classList.toggle('hidden', input.value === ''); }
input.addEventListener('input', syncClear);
clear.addEventListener('click', function () { input.value = ''; suggest.close(); syncClear(); input.focus(); });
function go(q, t, push) {
query = q;
type = t;
input.value = q;
syncClear();
if (push) history.pushState(null, '', 'search.html?q=' + encodeURIComponent(q) + (t !== 'all' ? '&type=' + t : ''));
render();
}
function fromLocation() {
var p = new URLSearchParams(location.search);
var t = p.get('type');
go((p.get('q') || '').trim(), TYPES.some(function (x) { return x[0] === t; }) ? t : 'all', false);
}
document.getElementById('search-form').addEventListener('submit', function (e) {
e.preventDefault();
suggest.close();
go(input.value.trim(), type, true);
});
document.getElementById('search-results').addEventListener('click', function (e) {
var a = e.target.closest('a[data-type]');
if (!a) return;
e.preventDefault();
go(query, a.getAttribute('data-type'), true);
});
window.addEventListener('popstate', fromLocation);
fromLocation();
})();
</script>
</body>
</html>