Ctrl K
ComunidadGuías…

mockups/notifications.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>Notifications — 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 antialiased flex flex-col">

<a href="#main-content" class="skip-link">Skip to main content</a>

<div id="cz-header"></div>
<script src="shell.js"></script>

<main id="main-content" tabindex="-1" class="max-w-[1400px] mx-auto px-6 pt-6 pb-20 flex-1 w-full">
  <h1 class="mb-5 text-2xl font-semibold tracking-tight">Notifications</h1>

  <div id="notifications-view" class="grid gap-6 md:grid-cols-[210px_1fr]">
    <aside aria-label="Notification filters">
      <nav class="flex flex-col gap-0.5" id="filters"></nav>
      <a href="account_settings.html#notifications" class="mt-3 flex items-center gap-2 px-2.5 py-1.5 rounded-md text-[13px] hover:bg-accent">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" class="text-muted-foreground" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
        <span>Notification settings</span>
      </a>
      <button type="button" id="shortcuts-open" class="mt-0.5 w-full flex items-center gap-2 px-2.5 py-1.5 rounded-md text-[13px] hover:bg-accent" aria-keyshortcuts="?"><span class="flex-1 text-left">Keyboard shortcuts</span><kbd class="font-mono text-[10px] px-1.5 py-0.5 bg-muted border border-border rounded-sm text-muted-foreground">?</kbd></button>
    </aside>

    <div class="min-w-0">
      <nav id="reasons" class="mb-3 flex flex-wrap items-center gap-1.5" aria-label="Filter by reason"></nav>
      <form id="notifications-form" onsubmit="return false" class="border border-border rounded-md bg-card overflow-hidden" aria-label="Notifications">
        <div id="bar-idle" class="flex items-center gap-3 px-4 min-h-[45px] border-b border-border bg-muted/40 text-[12px] text-muted-foreground">
          <input type="checkbox" id="select-all-idle" class="shrink-0 size-[15px] accent-primary cursor-pointer" aria-label="Select all notifications on this page"/>
          <span><span class="font-mono" id="unread-count">0</span> unread</span>
          <span class="flex-1"></span>
          <button type="button" id="mark-all" class="h-7 px-2.5 text-[12px] text-foreground border border-border rounded-md bg-card hover:bg-accent inline-flex items-center gap-1.5">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
            Mark all as read
          </button>
        </div>
        <div id="bar-selected" hidden class="flex items-center gap-3 px-4 min-h-[45px] border-b border-border bg-muted/40 text-[13px]">
          <input type="checkbox" id="select-all-sel" class="shrink-0 size-[15px] accent-primary cursor-pointer" aria-label="Select all notifications on this page"/>
          <span class="font-semibold"><span id="sel-count">0</span> selected</span>
          <span class="flex-1"></span>
          <button type="button" id="bulk-done" class="h-7 px-2.5 text-[12px] border border-border rounded-md bg-card hover:bg-accent inline-flex items-center gap-1.5">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
            Done
          </button>
          <button type="button" id="bulk-mute" title="Mute the selected threads" class="h-7 px-2.5 text-[12px] border border-border rounded-md bg-card hover:bg-accent inline-flex items-center gap-1.5">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"><path d="M13.7 21a2 2 0 01-3.4 0M18.6 13A17 17 0 0118 8M6.3 6.3A6 6 0 006 8c0 7-3 9-3 9h14M18 8a6 6 0 00-9.3-5M1 1l22 22"/></svg>
            Unsubscribe
          </button>
        </div>
        <div id="groups"></div>
      </form>
      <section id="empty" hidden class="border border-border rounded-md bg-card overflow-hidden">
        <div class="px-4 py-16 text-center"><p class="text-[13px] text-muted-foreground" id="empty-msg">You're all caught up.</p></div>
      </section>

      <nav class="mt-5 flex items-center justify-center gap-1" aria-label="Pagination" id="pager">
        <span class="h-8 px-2 grid place-items-center text-[13px] text-muted-foreground opacity-50" aria-disabled="true">‹ Previous</span>
        <span class="h-8 min-w-8 px-2 grid place-items-center rounded-md text-[13px] bg-primary text-primary-foreground" aria-current="page">1</span>
        <a href="notifications.html" class="h-8 min-w-8 px-2 grid place-items-center rounded-md text-[13px] hover:bg-accent">2</a>
        <a href="notifications.html" class="h-8 min-w-8 px-2 grid place-items-center rounded-md text-[13px] hover:bg-accent">3</a>
        <a href="notifications.html" class="h-8 px-2 grid place-items-center rounded-md text-[13px] hover:bg-accent">Next ›</a>
      </nav>
      <p class="mt-3 text-center text-[12px] text-muted-foreground" id="range"></p>
    </div>
  </div>
</main>

<dialog id="notif-shortcuts" class="rounded-md border border-border bg-card text-foreground p-6"><h2 class="font-semibold">Keyboard shortcuts</h2><dl class="flex flex-col gap-2 mt-4"><div class="flex items-center justify-between gap-4"><dt class="text-[13px]">Next notification</dt><dd><kbd class="font-mono text-[11px] min-w-6 px-1.5 py-0.5 inline-grid place-items-center bg-muted border border-border rounded-sm">j</kbd></dd></div><div class="flex items-center justify-between gap-4"><dt class="text-[13px]">Previous notification</dt><dd><kbd class="font-mono text-[11px] min-w-6 px-1.5 py-0.5 inline-grid place-items-center bg-muted border border-border rounded-sm">k</kbd></dd></div><div class="flex items-center justify-between gap-4"><dt class="text-[13px]">Open the thread</dt><dd><kbd class="font-mono text-[11px] min-w-6 px-1.5 py-0.5 inline-grid place-items-center bg-muted border border-border rounded-sm">Enter</kbd></dd></div><div class="flex items-center justify-between gap-4"><dt class="text-[13px]">Select or deselect</dt><dd><kbd class="font-mono text-[11px] min-w-6 px-1.5 py-0.5 inline-grid place-items-center bg-muted border border-border rounded-sm">x</kbd></dd></div><div class="flex items-center justify-between gap-4"><dt class="text-[13px]">Mark as done</dt><dd><kbd class="font-mono text-[11px] min-w-6 px-1.5 py-0.5 inline-grid place-items-center bg-muted border border-border rounded-sm">e</kbd></dd></div><div class="flex items-center justify-between gap-4"><dt class="text-[13px]">Show these shortcuts</dt><dd><kbd class="font-mono text-[11px] min-w-6 px-1.5 py-0.5 inline-grid place-items-center bg-muted border border-border rounded-sm">?</kbd></dd></div></dl><form method="dialog" class="mt-4"><button>Close</button></form></dialog>
<div id="cz-footer"></div>

<script>
(function () {
  var ICON = {
    review: ['text-primary', '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>'],
    mention: ['text-warning', '<circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 006 0v-1a10 10 0 10-3.92 7.94"/>'],
    merged: ['text-[hsl(270_70%_70%)]', '<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"/>', true],
    discussion: ['text-primary', '<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>'],
    issue: ['text-destructive', '<circle cx="12" cy="12" r="9"/><path d="M9 9l6 6M15 9l-6 6"/>'],
    transfer: ['text-primary', '<path d="M4 8h14l-4-4M20 16H6l4 4"/>'],
    comment: ['text-muted-foreground', '<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>']
  };
  var MUTE = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"><path d="M13.7 21a2 2 0 01-3.4 0M18.6 13A17 17 0 0118 8M6.3 6.3A6 6 0 006 8c0 7-3 9-3 9h14M18 8a6 6 0 00-9.3-5M1 1l22 22"/></svg>';
  var CHECK = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>';
  var BTN = 'size-7 grid place-items-center border border-border rounded-md bg-card text-muted-foreground hover:text-foreground hover:bg-accent';

  var N = [
    { id: 1, repo: 'mkappworks/cloudzilla', type: 'review', title: 'Add OAuth scopes to access tokens', text: '@daisy reviewed PR #341', by: 'daisy', when: '12m ago', read: false, thread: true },
    { id: 2, repo: 'mkappworks/cloudzilla', type: 'mention', title: 'Flaky test on push', text: '@yumi mentioned you', by: 'yumi', when: '1h ago', read: false, thread: true },
    { id: 3, repo: 'mkappworks/cloudzilla', type: 'merged', title: 'Cache avatar lookups per request', text: '@daisy merged PR #338', by: 'daisy', when: '3h ago', read: true, thread: true },
    { id: 4, repo: 'mkappworks/ingest-pipeline', type: 'comment', title: 'Replay window UI', text: '@kiyo commented on issue #298', by: 'kiyo', when: 'yesterday', read: false, thread: true },
    { id: 5, repo: 'acme-corp/api', type: 'review', title: 'Rate-limit middleware', text: '@kiyo reviewed PR #319', by: 'kiyo', when: 'yesterday', read: true, thread: true },
    { id: 6, repo: 'acme-corp/api', type: 'transfer', title: '', text: '@daisy wants to transfer a repository to you', by: 'daisy', when: 'Nov 3', read: false, thread: false },
    { id: 7, repo: 'openforge/runner', type: 'issue', title: 'Runner leaks tmp dirs', text: '@kiyo closed issue #41', by: 'kiyo', when: 'Nov 2', read: true, thread: true },
    { id: 8, repo: 'stitch-labs/loom', type: 'discussion', title: 'Roadmap for 0.4', text: '@yumi replied in a discussion', by: 'yumi', when: 'Nov 1', read: true, thread: true }
  ];
  var filter = 'inbox', reason = '', selected = {};
  N[0].count = 3; N[1].count = 2; N[0].reasons = ['review_requested', 'subscribed']; N[1].reasons = ['mention', 'assigned']; N[3].reasons = ['assigned', 'subscribed'];
  var NAV = 'flex items-center justify-between px-2.5 py-1.5 rounded-md text-[13px] hover:bg-accent';

  function visible() { return N.filter(function (n) { return (filter === 'inbox' || (filter === 'unread') === !n.read) && (!reason || (n.reasons || []).indexOf(reason) >= 0); }); }
  function count(f) { return N.filter(function (n) { return (f === 'inbox' || (f === 'unread') === !n.read) && (!reason || (n.reasons || []).indexOf(reason) >= 0); }).length; }

  function row(n) {
    var title = n.title
      ? '<span class="text-[13.5px] ' + (n.read ? 'text-muted-foreground' : 'font-semibold') + '">' + n.title + '</span><span class="text-[12px] text-muted-foreground ml-2">' + n.text + '</span>'
      : '<span class="text-[13.5px] ' + (n.read ? 'text-muted-foreground' : 'font-semibold') + '">' + n.text + '</span>';
    var ic = ICON[n.type];
    var box = '<span class="shrink-0 w-7 h-7 rounded-[6px] grid place-items-center bg-accent border border-border ' + (n.read ? 'opacity-65' : ic[0]) + '"><svg width="14" height="14" viewBox="0 0 ' + (ic[2] ? '16 16' : '24 24') + '" ' + (ic[2] ? 'fill="currentColor"' : 'fill="none" stroke="currentColor" stroke-width="1.75"') + ' aria-hidden="true">' + ic[1] + '</svg></span>';
    var done = n.read ? '' : '<button type="button" class="' + BTN + '" aria-label="Done" title="Done" data-done="' + n.id + '">' + CHECK + '</button>';
    var mute = n.thread
      ? '<button type="button" class="' + BTN + '" aria-label="Unsubscribe: mute this thread" title="Unsubscribe: mute this thread. Repo watch unchanged." data-mute="' + n.id + '">' + MUTE + '</button>'
      : '<button type="button" class="' + BTN + ' opacity-40 cursor-not-allowed hover:bg-card hover:text-muted-foreground" aria-label="Unsubscribe: mute this thread" title="A repository transfer has no thread to unsubscribe from" disabled>' + MUTE + '</button>';
    return '<li class="group border-b border-border last:border-b-0"><div class="flex items-center gap-3 px-4 py-2 min-h-[46px] hover:bg-accent transition-colors has-[:checked]:bg-primary/10 ' + (n.read ? '' : 'bg-primary/[0.03]') + '">' +
      (n.read ? '<span class="shrink-0 w-[7px] h-[7px] rounded-full border border-border" aria-hidden="true"></span>' : '<span class="shrink-0 w-[7px] h-[7px] rounded-full bg-primary" aria-label="Unread"></span>') +
      '<input type="checkbox" name="ids" value="' + n.id + '" class="shrink-0 size-[15px] accent-primary cursor-pointer" aria-label="Select notification"' + (selected[n.id] ? ' checked' : '') + '/>' + box +
      '<a href="' + (n.type === 'review' || n.type === 'merged' ? 'pr.html' : 'issue_detail.html') + '" class="min-w-0 flex-1 truncate">' + title + '</a>' +
      (n.count > 1 ? '<span class="shrink-0 h-5 min-w-5 px-1.5 grid place-items-center rounded-full border border-border bg-muted font-mono text-[11px] text-muted-foreground" title="' + n.count + ' notifications on this thread">' + n.count + '</span>' : '') + '<div class="group/right relative shrink-0 w-[150px] h-7 flex items-center justify-end gap-3"><span class="relative inline-grid place-items-center h-5 w-5 shrink-0 overflow-hidden rounded-sm bg-accent border border-border text-[10px] font-medium" aria-hidden="true">' + n.by[0].toUpperCase() + '</span>' +
      '<time class="w-16 text-right text-[12px] text-muted-foreground group-hover:invisible group-has-[:focus-visible]/right:invisible">' + n.when + '</time>' +
      '<div class="absolute right-0 top-0 h-7 flex items-center gap-1 opacity-0 group-hover:opacity-100 focus-within:opacity-100">' + done + mute + '</div></div></div></li>';
  }

  function render() {
    var rows = visible(), seen = [], html = '';
    document.getElementById('reasons').innerHTML = [['', 'All'], ['mention', 'Mentions'], ['review_requested', 'Review requested'], ['assigned', 'Assigned'], ['subscribed', 'Subscribed']].map(function (r) { return '<a href="#" data-reason="' + r[0] + '" class="h-7 px-3 inline-flex items-center rounded-full border text-[12px] transition-colors ' + (reason === r[0] ? 'border-primary/40 bg-primary/10 text-foreground font-medium' : 'border-border bg-card text-muted-foreground hover:text-foreground hover:bg-accent') + '">' + r[1] + '</a>'; }).join('');
    document.getElementById('mark-all').hidden = !!reason;
    rows.forEach(function (n) { if (seen.indexOf(n.repo) < 0) seen.push(n.repo); });
    seen.forEach(function (repo) {
      html += '<h2 class="px-4 py-2 text-[13px] font-semibold font-mono bg-muted/40 border-b border-border">' + repo + '</h2><ol>' + rows.filter(function (n) { return n.repo === repo; }).map(row).join('') + '</ol>';
    });
    document.getElementById('groups').innerHTML = html;
    document.getElementById('notifications-form').hidden = !rows.length;
    document.getElementById('empty').hidden = rows.length > 0;
    document.getElementById('empty-msg').textContent = filter === 'read' ? 'No read notifications.' : "You're all caught up.";
    document.getElementById('pager').hidden = !rows.length;
    document.getElementById('unread-count').textContent = count('unread');
    document.getElementById('range').innerHTML = rows.length ? '<span class="font-mono">1–' + rows.length + '</span> of <span class="font-mono">' + rows.length + '</span>' : '';
    document.getElementById('filters').innerHTML = [['Inbox', 'inbox'], ['Unread', 'unread'], ['Read', 'read']].map(function (f) {
      var on = f[1] === filter;
      return '<a href="#" data-filter="' + f[1] + '" class="' + NAV + (on ? ' bg-accent font-semibold' : '') + '"' + (on ? ' aria-current="page"' : '') + '><span>' + f[0] + '</span><span class="font-mono text-[12px] text-muted-foreground">' + count(f[1]) + '</span></a>';
    }).join('');
    syncBar();
  }
  function syncBar() {
    var n = Object.keys(selected).filter(function (k) { return selected[k]; }).length, total = visible().length;
    document.getElementById('bar-idle').hidden = n > 0;
    document.getElementById('bar-selected').hidden = n === 0;
    document.getElementById('sel-count').textContent = n;
    document.getElementById('select-all-sel').checked = n === total && total > 0;
    document.getElementById('select-all-idle').checked = false;
  }
  function byId(id) { return N.filter(function (n) { return n.id === +id; })[0]; }
  function drop(ids) { N = N.filter(function (n) { return ids.indexOf(n.id) < 0; }); selected = {}; render(); }
  function selIds() { return Object.keys(selected).filter(function (k) { return selected[k]; }).map(Number); }

  document.getElementById('filters').addEventListener('click', function (e) {
    var a = e.target.closest('a'); if (!a) return; e.preventDefault(); filter = a.dataset.filter; selected = {}; render();
  });
  document.getElementById('groups').addEventListener('change', function (e) {
    if (e.target.name !== 'ids') return; selected[e.target.value] = e.target.checked; syncBar();
  });
  document.getElementById('groups').addEventListener('click', function (e) {
    var d = e.target.closest('[data-done]'), m = e.target.closest('[data-mute]');
    if (d) { var index = Array.from(document.querySelectorAll('#groups li')).indexOf(d.closest('li')); byId(d.dataset.done).read = true; render(); var rows = document.querySelectorAll('#groups li'); if (rows.length) rows[Math.min(index, rows.length - 1)].querySelector('a').focus(); }
    if (m) drop([+m.dataset.mute]);
  });
  function selectAll(on) { visible().forEach(function (n) { selected[n.id] = on; }); render(); }
  document.getElementById('select-all-idle').addEventListener('change', function () { selectAll(this.checked); });
  document.getElementById('select-all-sel').addEventListener('change', function () { selectAll(this.checked); });
  document.getElementById('mark-all').addEventListener('click', function () { N.forEach(function (n) { n.read = true; }); render(); });
  document.getElementById('bulk-done').addEventListener('click', function () { selIds().forEach(function (id) { byId(id).read = true; }); selected = {}; render(); });
  document.getElementById('bulk-mute').addEventListener('click', function () { drop(selIds()); });
  document.getElementById('reasons').addEventListener('click', function(e) { var a = e.target.closest('[data-reason]'); if (!a) return; e.preventDefault(); reason = a.dataset.reason; selected = {}; render(); });
  var dialog = document.getElementById('notif-shortcuts');
  document.getElementById('shortcuts-open').addEventListener('click', function() { dialog.showModal(); });
  document.addEventListener('keydown', function(e) { if (dialog.open || e.metaKey || e.ctrlKey || e.altKey || e.target.matches('input:not([type=checkbox]), textarea, select')) return; var rows = Array.from(document.querySelectorAll('#groups li')), i = rows.findIndex(function(r) { return r.contains(document.activeElement); }); if (e.key === '?') dialog.showModal(); else if (e.key === 'j' || e.key === 'k') { var row = rows[Math.max(0, Math.min(rows.length - 1, i + (e.key === 'j' ? 1 : -1)))]; if (row) row.querySelector('a').focus(); } else if (e.key === 'e' && i >= 0) { var done = rows[i].querySelector('[data-done]'); if (done) done.click(); } else if (e.key === 'x' && i >= 0) { var box = rows[i].querySelector('input'); box.click(); } else return; e.preventDefault(); });
  render();
})();
</script>

</body>
</html>