Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions web/src/components/CatalogCharts.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,16 @@
<h3 id="chart-pulse">Last 14 days</h3>
<p>Unique visitors and cloners summed across this catalog.</p>
</header>
<div class="pulse-chart" aria-hidden="true">
<div class="pulse-chart">
{#each pulse as day}
<div class="pulse-col">
<p class="pulse-tip">
<span>{formatCount(day.viewUniques)}</span>
<span class="is-clone">{formatCount(day.cloneUniques)}</span>
</p>
<div class="pulse-pair">
<div class="traffic-bar" style={`height:${day.viewHeight}`}></div>
<div class="traffic-bar traffic-bar-clone" style={`height:${day.cloneHeight}`}></div>
<div class="traffic-bar" style={`height:${day.viewHeight}`} title={`${formatCount(day.viewUniques)} unique visitor${day.viewUniques === 1 ? "" : "s"}`}></div>
<div class="traffic-bar traffic-bar-clone" style={`height:${day.cloneHeight}`} title={`${formatCount(day.cloneUniques)} unique cloner${day.cloneUniques === 1 ? "" : "s"}`}></div>
</div>
<span>{day.label}</span>
</div>
Expand Down
7 changes: 7 additions & 0 deletions web/src/components/CatalogCharts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,11 @@ describe("CatalogCharts", () => {
expect(within(quiet).getByRole("link", { name: "cais" })).toHaveAttribute("href", "https://github.com/puppe1990/cais");
expect(within(screen.getByRole("region", { name: "Unique visitors by language" })).queryByRole("link")).not.toBeInTheDocument();
});

test("pulse bars show unique counts on hover", () => {
render(CatalogCharts, { props: { repos } });
const pulse = screen.getByRole("region", { name: "Last 14 days" });
expect(within(pulse).getByTitle("8 unique visitors")).toBeInTheDocument();
expect(within(pulse).getByTitle("1 unique cloner")).toBeInTheDocument();
});
});
9 changes: 9 additions & 0 deletions web/src/components/CatalogSpinner.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script>
export let size = "md";
</script>

<div class="atlas-spinner" class:is-sm={size === "sm"} aria-hidden="true">
<span class="atlas-spinner-orbit"></span>
<span class="atlas-spinner-arc"></span>
<span class="atlas-spinner-core"></span>
</div>
6 changes: 6 additions & 0 deletions web/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import { createInertiaApp } from '@inertiajs/svelte'
import { mount } from 'svelte'

createInertiaApp({
progress: {
delay: 80,
color: "#d4a06a",
includeCSS: true,
showSpinner: false,
},
resolve: (name) => {
const pages = import.meta.glob('./pages/**/*.svelte', { eager: true })
const page = pages[`./pages/${name}.svelte`]
Expand Down
33 changes: 21 additions & 12 deletions web/src/pages/Home.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import RepoCard from "../components/RepoCard.svelte";
import CatalogCharts from "../components/CatalogCharts.svelte";
import CatalogPager from "../components/CatalogPager.svelte";
import CatalogSpinner from "../components/CatalogSpinner.svelte";
import SortModal from "../components/SortModal.svelte";
import { applyFilters, languageOptions } from "../lib/filter.js";
import { sliceCatalogPage } from "../lib/catalogPage.js";
Expand Down Expand Up @@ -61,7 +62,10 @@

function go(path) {
loading = true;
router.get(path, {}, { onFinish: () => { loading = false } });
router.get(path, {}, {
onStart: () => { loading = true },
onFinish: () => { loading = false },
});
}

function submit() {
Expand Down Expand Up @@ -128,7 +132,10 @@
<div class="search-shell">
<span class="search-prefix" aria-hidden="true">@</span>
<input id="username" name="username" type="text" spellcheck="false" autocapitalize="off" autocorrect="off" placeholder="username" bind:value={username}>
<button type="submit" id="search-btn">Look up</button>
<button type="submit" id="search-btn" disabled={loading} aria-busy={loading}>
{#if loading}<CatalogSpinner size="sm" />{/if}
Look up
</button>
</div>
<p class="hints">
<span>Try</span>
Expand Down Expand Up @@ -258,22 +265,23 @@
</section>
{/if}

{#if loading}
<div id="loader" class="loader-wrap">
<div class="loader" aria-hidden="true"></div>
<p id="loader-copy">Reading the public graph…</p>
</div>
{/if}
<div class="catalog-stage" class:is-busy={loading} aria-busy={loading}>
{#if loading}
<div id="loader" class="loader-overlay" role="status" aria-labelledby="loader-copy" aria-live="polite">
<CatalogSpinner />
<p id="loader-copy">Reading the public graph…</p>
</div>
{/if}

{#if populated && !loading}
{#if populated}
{#if hasTraffic}
<div class="view-tabs" role="tablist" aria-label="Catalog view">
<button type="button" class="view-tab" role="tab" id="tab-catalog" aria-controls="projects" aria-selected={view === "catalog"} on:click={() => (view = "catalog")}>Catalog</button>
<button type="button" class="view-tab" role="tab" id="tab-charts" aria-controls="panel-charts" aria-selected={view === "charts"} on:click={() => (view = "charts")}>Charts</button>
</div>
{/if}
{#if view === "catalog" || !hasTraffic}
<div id="projects" role={hasTraffic ? "tabpanel" : undefined} aria-labelledby={hasTraffic ? "tab-catalog" : undefined}>
<div id="projects" class="catalog-panel" role={hasTraffic ? "tabpanel" : undefined} aria-labelledby={hasTraffic ? "tab-catalog" : undefined}>
{#if visible.length === 0}
<div class="projects">
<div class="empty">
Expand All @@ -296,11 +304,12 @@
{/if}
</div>
{:else}
<div id="panel-charts" role="tabpanel" aria-labelledby="tab-charts">
<div id="panel-charts" class="catalog-panel" role="tabpanel" aria-labelledby="tab-charts">
<CatalogCharts repos={visible} />
</div>
{/if}
{/if}
{/if}
</div>
</div>

<SortModal bind:open={sortOpen} bind:sortBy bind:sortOrder bind:sortThen bind:sortThenOrder {hasTraffic} />
8 changes: 8 additions & 0 deletions web/src/pages/Home.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ describe("Home", () => {
expect(screen.getByLabelText("GitHub username")).toBeInTheDocument();
});

test("shows a catalog spinner while looking up a username", async () => {
render(Home, { props: { populated: false, repos: [], orgs: [], profile: {}, source: {}, error: "" } });
await fireEvent.input(screen.getByLabelText("GitHub username"), { target: { value: "octocat" } });
await fireEvent.click(screen.getByRole("button", { name: "Look up" }));
expect(screen.getByRole("status", { name: /Reading the public graph/ })).toBeInTheDocument();
expect(document.querySelector(".atlas-spinner")).toBeInTheDocument();
});

test("renders a populated profile and repo card", () => {
render(Home, {
props: {
Expand Down
162 changes: 153 additions & 9 deletions web/static/css/atlas.css
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,10 @@ html[data-theme="light"] .theme-toggle .icon-moon {
}

#search-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
margin: 0.4rem;
border: 0;
border-radius: 999px;
Expand Down Expand Up @@ -907,21 +911,111 @@ a.stat:hover {
box-shadow: 0 0 0 3px var(--copper-dim);
}

.loader-wrap {
.catalog-stage {
position: relative;
min-height: 4rem;
}

.catalog-stage.is-busy > :not(.loader-overlay) {
opacity: 0.32;
filter: blur(1.4px);
pointer-events: none;
transition:
opacity 0.45s ease,
filter 0.45s ease;
}

.loader-overlay {
position: absolute;
inset: 0;
z-index: 4;
display: grid;
place-content: center;
place-items: center;
gap: 0.85rem;
padding: 3.5rem 0;
gap: 0.95rem;
min-height: 12rem;
padding: 2.5rem 0 3rem;
color: var(--ink-dim);
animation: rise 0.4s ease both;
}

.loader {
width: 36px;
height: 36px;
.loader-overlay #loader-copy {
margin: 0;
font-family: var(--font-mono);
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-mute);
}

.atlas-spinner {
position: relative;
width: 3.35rem;
height: 3.35rem;
}

.atlas-spinner.is-sm {
width: 0.95rem;
height: 0.95rem;
}

.atlas-spinner-orbit,
.atlas-spinner-arc,
.atlas-spinner-core {
position: absolute;
display: block;
border-radius: 50%;
border: 2px solid var(--line-strong);
}

.atlas-spinner-orbit {
inset: 0;
border: 1px dashed color-mix(in srgb, var(--copper) 55%, transparent);
animation: spin 5.2s linear infinite;
}

.atlas-spinner-arc {
inset: 7px;
border: 2px solid transparent;
border-top-color: var(--copper);
animation: spin 0.8s linear infinite;
border-right-color: var(--copper-hot);
animation: spin 0.85s cubic-bezier(0.45, 0.05, 0.2, 1) infinite;
}

.atlas-spinner.is-sm .atlas-spinner-arc {
inset: 1px;
border-width: 1.5px;
}

.atlas-spinner-core {
inset: 38%;
background: var(--copper);
box-shadow: 0 0 14px var(--glow-a);
animation: pulse-core 1.15s ease-in-out infinite;
}

.atlas-spinner.is-sm .atlas-spinner-core {
inset: 32%;
box-shadow: none;
}

.catalog-panel {
animation: rise 0.45s ease both;
}

#search-btn:disabled {
opacity: 0.72;
cursor: wait;
}

#nprogress .bar {
background: var(--copper) !important;
height: 2px;
}

#nprogress .peg {
box-shadow:
0 0 10px var(--copper),
0 0 5px var(--copper) !important;
}

.projects {
Expand Down Expand Up @@ -1306,11 +1400,12 @@ a.rank-name:focus-visible {
align-items: flex-end;
justify-content: flex-start;
gap: 0.45rem;
height: 148px;
height: 168px;
overflow-x: auto;
}

.pulse-col {
position: relative;
flex: 1 1 1.7rem;
max-width: 2.4rem;
min-width: 1.35rem;
Expand All @@ -1319,6 +1414,42 @@ a.rank-name:focus-visible {
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding-top: 1.45rem;
}

.pulse-tip {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%) translateY(4px);
display: flex;
gap: 0.28rem;
margin: 0;
padding: 0.12rem 0.32rem;
border-radius: 6px;
background: var(--bg-elev);
border: 1px solid var(--line-strong);
box-shadow: var(--card-shadow);
font-family: var(--font-mono);
font-size: 0.62rem;
line-height: 1;
color: var(--copper);
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition:
opacity 0.16s ease,
transform 0.16s ease;
}

.pulse-tip .is-clone {
color: var(--moss);
}

.pulse-col:hover .pulse-tip,
.pulse-col:focus-within .pulse-tip {
opacity: 1;
transform: translateX(-50%) translateY(0);
}

.pulse-pair {
Expand All @@ -1334,6 +1465,7 @@ a.rank-name:focus-visible {
flex: 1;
max-width: 9px;
min-height: 0;
cursor: pointer;
}

.pulse-col span {
Expand Down Expand Up @@ -1421,6 +1553,18 @@ a.rank-name:focus-visible {
}
}

@keyframes pulse-core {
0%,
100% {
transform: scale(0.82);
opacity: 0.7;
}
50% {
transform: scale(1);
opacity: 1;
}
}

@media (max-width: 860px) {
.app {
padding-top: 2.6rem;
Expand Down
Loading