Ctrl K
ComunidadGuías…

mockups/search.html

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<!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>