website

#astro#js#html#css

git clone https://git.pyrossh.dev/website

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


assets/css/repos.css
/* Repo browsing pages (readme/files/commits/blame). Restored from the
   original Astro-era RepoLayout/FileLayout/Commit/RecursiveList components
   (see git history at 7325531^), with the tab nav restyled as a bordered
   tab bar and a couple of small crates.io-inspired touches (clone command
   as its own code box, tags as pills) layered on top.

   Font sizes on this page follow one small scale instead of one-off
   values, so nothing reads as randomly bigger/smaller than its neighbour:
     0.85rem — small badges (tags, pull-right meta, file-size hints)
     0.9rem  — code/monospace content, nav links and sub-headings alike
               (pre/code-view, file tree, blame, file title)
     0.95rem — box body text (.repo, .event)
     var(--fs-repo-title) — page-level heading (.repo h1), fluid 1.3–1.6rem
   All of them are rem-based, not em, so nesting depth can't change the
   rendered size — that's what made the old code-view/blame font sizes
   inconsistent (em compounds with whatever ancestor happens to wrap it).

   Everything below uses plain CSS nesting rather than @scope: .repo,
   .file-layout, .event, .file-explorer and table.blame are all real,
   always-present classes/tags on these pages (none of them are wrapped
   in a custom element anymore), so nesting under them scopes their
   descendants exactly the way @scope would, without needing a scope root
   selector that has to be kept in sync with whatever markup happens to
   wrap it. */

.repo {
  text-overflow: ellipsis;
  overflow: hidden;
  padding: 0.5rem;
  background: var(--color-box-bg);
  color: var(--color-text);
  font-size: 0.95rem;
  padding: 1rem;

  a {
    color: var(--color-link);
  }

  .header {
    display: flex;
    flex-direction: row;
    align-items: baseline;

    @media (max-width: 720px) {
      flex-direction: column;
      align-items: start;
    }
  }

  h1 {
    font-size: var(--fs-repo-title);
    font-weight: 500;
    line-height: 1;
    margin-bottom: 0.1rem;
    flex: 1;
  }

  .repo-version {
    font-size: 1.1rem;
    font-weight: 500;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    color: var(--color-text);
    margin-left: 0.5rem;
  }

  .tags {
    margin-top: 0.5rem;
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
  }

  .tag {
    color: var(--color-tag);
    padding: 0.15rem 0.5rem 0.5rem 0;
    border-radius: 4px;
    font-size: 0.95rem;
    font-weight: 500;
  }

  h3.clone-cmd {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-weight: 400;
    font-size: var(--fs-repo-clone);
    padding: 0.4rem;
    background-color: var(--color-pre-bg);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    overflow-x: auto;
    white-space: pre;
    width: fit-content;
    margin-top: 0.2rem;

    .copy-btn {
      flex-shrink: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 1.6rem;
      height: 1.6rem;
      padding: 0;
      border: none;
      border-radius: 4px;
      background: none;
      color: var(--color-text);
      opacity: 0.6;
      cursor: pointer;

      &:hover {
        opacity: 1;
        background: var(--color-box-bg);
      }

      .icon-check {
        display: none;
      }

      &.copied {
        opacity: 1;
        color: var(--color-tag);

        .icon-copy {
          display: none;
        }

        .icon-check {
          display: block;
        }
      }
    }
  }

  .headerExtension h2 {
    display: flex;
    flex: 1;
    padding-top: 0.3rem;
    font-size: var(--fs-repo-subtitle);
    font-weight: 400;
  }

  hr {
    margin-top: 0.5rem;
    border: none;
    border-top: 1px solid var(--color-text);
  }

  .nav {
    display: flex;
    align-items: center;
    margin-top: 0.5rem;

    span {
      padding: 0 0.75rem;
      color: var(--color-text);
    }

    a {
      font-size: 1rem;
      font-weight: 500;
      cursor: pointer;
      color: var(--color-link);
      text-decoration: none;

      &.active-tab {
        text-decoration: underline;
        text-underline-offset: 4px;
      }
    }
  }
}

#repo-content {
  margin-top: 0.75rem;

  /* README (repos/repo.njk) — same box treatment as the file explorer
     panes below, so it doesn't sit as bare unstyled text against the
     page background. Scoped to #repo-content so blog posts (which also
     use .markdown-body) are untouched. */
  .markdown-body {
    background: var(--color-box-bg);
    padding: 1rem 1.25rem;
  }
}

/* Commit list / commit detail header (Commit.astro) — repos/commit.njk,
   commits.njk, file-history.njk. pre.commit is nested here too since
   it's always .event's child, never used alone. */
.event {
  background-color: var(--color-box-bg);
  color: var(--color-text);
  text-overflow: ellipsis;
  overflow: hidden;
  padding: 0.6rem 0.75rem;
  margin: 0.5rem 0;
  font-size: 0.95rem;

  a {
    color: var(--color-link);
  }

  strong {
    font-weight: 500;
  }

  .pull-right {
    font-size: 0.85rem;
    font-weight: 400;
    float: right;
  }

  pre.commit {
    font-family: inherit;
    white-space: pre-wrap;
    margin-top: 0.35rem;
  }
}

/* Two-pane IDE-style file explorer: a persistent tree sidebar + a content
   pane, each its own rounded box with a gap between them rather than a
   single shared border — the gap is what visually separates them, so
   they don't also need to differ in colour. The tree lives outside
   #file-pane, so htmx swaps of #file-pane (clicking a file) or
   #file-content (Contents/History/Blame) never touch the sidebar — it
   just stays where it is, already expanded, no reload.

   Fixed-height explorer: both panes stretch to the same height and scroll
   independently, so scrolling a long file never scrolls the tree out of
   view (or the whole page) — closer to a real IDE split view than a page
   that just keeps growing with the file's content. */
.file-explorer {
  display: flex;
  align-items: stretch;
  gap: 0.5rem;
  height: 80vh;

  /* File tree: flat rows (no per-item card background), a rotating
     triangle for expand/collapse instead of an emoji folder glyph, and a
     left accent border + tint on the active file instead of a filled
     pill. */
  .file-explorer-tree {
    width: 260px;
    flex-shrink: 0;
    height: 100%;
    overflow-y: auto;
    padding: 0.6rem;
    background: var(--color-box-bg);
    border-radius: 6px;
    font-size: 0.9rem;

    ul {
      list-style: none;
      margin: 0 0 0 1.1rem;
      padding: 0;
    }

    > ul {
      margin-left: 0;
    }

    li {
      margin: 0;

      &.active-file,
      & > a.active-file {
        color: var(--color-link);
      }
    }

    summary,
    li > a {
      display: flex;
      align-items: center;
      gap: 0.4rem;
      padding: 0.2rem 0.35rem;
      border-radius: 4px;
      border-left: 2px solid transparent;
      cursor: pointer;
      text-decoration: none;
      color: var(--color-text);
      line-height: 1.6;
    }

    summary {
      list-style: none;

      &::-webkit-details-marker {
        display: none;
      }

      &::before {
        content: "";
        flex-shrink: 0;
        width: 0;
        height: 0;
        border-style: solid;
        border-width: 0.3em 0 0.3em 0.4em;
        border-color: transparent transparent transparent currentColor;
        opacity: 0.5;
        transition: transform 120ms ease;
      }
    }

    details[open] > summary::before {
      transform: rotate(90deg);
    }

    summary svg,
    li > a svg {
      flex-shrink: 0;
      width: 1em;
      height: 1em;
    }

    summary:hover,
    li > a:hover {
      background-color: var(--color-pre-bg);
    }

    li > a.active-file {
      color: var(--color-link);
      background-color: var(--color-highlight-bg);
      border-left-color: var(--color-link);
      font-weight: 500;
    }

    .file-size {
      margin-left: auto;
      opacity: 0.5;
      font-size: 0.85rem;
    }
  }

  .file-explorer-main {
    flex: 1;
    min-width: 0;
    height: 100%;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--color-box-bg);
    border-radius: 6px;
    overflow: hidden;

    &:has(.file-explorer-placeholder) {
      align-items: center;
      justify-content: center;
    }

    /* #file-content padding/background: when it holds actual code (a
       plain <pre>), let the box-bg colour fill the whole pane
       edge-to-edge instead of floating as a small inset card — closer to
       a real code viewer's continuous code surface. box-bg (not pre-bg)
       so it reads as one calm surface with the file-layout header above
       it, rather than the near-black pre-bg used for one-off code
       snippets elsewhere on the site. */
    #file-content {
      flex: 1;
      overflow-y: auto;
      padding: 0.85rem;

      &:has(> pre) {
        padding: 0;
        background: var(--color-box-bg);

        pre {
          margin: 0;
          background: transparent;
          border-radius: 0;
        }
      }
    }
  }

  .file-explorer-placeholder {
    color: var(--color-text);
    opacity: 0.6;
    padding: 1rem 0;
  }

  @media (max-width: 720px) {
    flex-direction: column;
    height: 85vh;

    .file-explorer-tree {
      width: 100%;
      height: auto;
      flex: 0 0 35%;
    }

    .file-explorer-main {
      width: 100%;
      height: auto;
      flex: 1 1 auto;
    }
  }
}

/* File view header — a compact icon+filename strip with the
   Contents/History/Blame tabs, sitting as the card's title bar rather
   than a full-width heading. */
.file-layout {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  flex-shrink: 0;
  gap: 0.5rem;
  padding: 0.6rem 0.85rem;
  background: var(--color-box-bg);
  border-bottom: 1px solid var(--color-pre-bg);

  .title {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    /* flex:1 (not the default 0 1 auto) so it's the one that gives up
       space when the row is tight — without this, a long filename could
       force the row to wrap .nav onto its own line instead of shrinking,
       since the wrap decision is made off each item's unshrunk
       hypothetical width, not its post-shrink width. */
    flex: 1 1 auto;
    min-width: 0;
    font-size: 0.9rem;
    font-weight: 500;
    margin-bottom: -4px;

    svg {
      flex-shrink: 0;
      width: 1rem;
      height: 1rem;
    }

    span {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }

  .nav {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    flex-shrink: 0;

    div:not(:has(a)) {
      display: none;
    }

    a {
      font-size: 0.9rem;
      font-weight: 500;
      padding: 0.3rem 0.1rem;
      margin-right: 0.75rem;
      cursor: pointer;
      color: var(--color-text);
      border-bottom: 2px solid transparent;

      &:hover {
        color: var(--color-link);
      }

      &[aria-current="true"] {
        color: var(--color-link);
        border-bottom-color: var(--color-link);
      }
    }
  }
}

/* Blame table — repos/file-blame.njk. */
table.blame {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: var(--color-pre-bg);
  border-radius: 4px;
  overflow: hidden;

  td {
    vertical-align: top;
    padding: 0.1rem 0.5rem;
    border-bottom: 1px solid var(--color-box-bg);
  }

  .blame-meta {
    white-space: nowrap;
    color: var(--color-link);
    width: 1%;
  }

  .blame-line-no {
    white-space: nowrap;
    opacity: 0.5;
    text-align: right;
    width: 1%;
  }

  .blame-content pre {
    margin: 0;
    white-space: pre-wrap;
  }
}

/* diff2html ships its own light/dark token sets as plain CSS custom
   properties on :root (see node_modules/diff2html/bundles/css) but only
   applies the dark set under a `.d2h-dark-color-scheme` class we never
   add. Remapping the base tokens it actually reads to their `--d2h-dark-*`
   counterparts under our own [data-theme="dark"] gets the same result
   without needing that class or a second copy of every diff2html rule.
   This re-themes a third-party library's own classes rather than styling
   one of our own, so it's left flat/global rather than nested. */
:root[data-theme="dark"] {
  --d2h-bg-color: var(--d2h-dark-bg-color);
  --d2h-border-color: var(--d2h-dark-border-color);
  --d2h-dim-color: var(--d2h-dark-dim-color);
  --d2h-line-border-color: var(--d2h-dark-line-border-color);
  --d2h-file-header-bg-color: var(--d2h-dark-file-header-bg-color);
  --d2h-file-header-border-color: var(--d2h-dark-file-header-border-color);
  --d2h-empty-placeholder-bg-color: var(--d2h-dark-empty-placeholder-bg-color);
  --d2h-empty-placeholder-border-color: var(--d2h-dark-empty-placeholder-border-color);
  --d2h-selected-color: var(--d2h-dark-selected-color);
  --d2h-ins-bg-color: var(--d2h-dark-ins-bg-color);
  --d2h-ins-border-color: var(--d2h-dark-ins-border-color);
  --d2h-ins-highlight-bg-color: var(--d2h-dark-ins-highlight-bg-color);
  --d2h-ins-label-color: var(--d2h-dark-ins-label-color);
  --d2h-del-bg-color: var(--d2h-dark-del-bg-color);
  --d2h-del-border-color: var(--d2h-dark-del-border-color);
  --d2h-del-highlight-bg-color: var(--d2h-dark-del-highlight-bg-color);
  --d2h-del-label-color: var(--d2h-dark-del-label-color);
  --d2h-change-del-color: var(--d2h-dark-change-del-color);
  --d2h-change-ins-color: var(--d2h-dark-change-ins-color);
  --d2h-info-bg-color: var(--d2h-dark-info-bg-color);
  --d2h-info-border-color: var(--d2h-dark-info-border-color);
  --d2h-change-label-color: var(--d2h-dark-change-label-color);
  --d2h-moved-label-color: var(--d2h-dark-moved-label-color);
}

.d2h-wrapper {
  color: var(--color-text);
}