mirror of
https://github.com/misenhower/splatoon2.ink.git
synced 2026-09-28 12:16:54 -05:00
Match Splatoon 3 directory browser styling
The initial Worker used a simplified renderer that omitted the established layout styles and folder/file icon markup.
This commit is contained in:
319
worker/index.js
319
worker/index.js
@@ -155,7 +155,7 @@ function breadcrumbHtml(prefix) {
|
||||
|
||||
let crumbs = ['<a href="/">assets</a>'];
|
||||
for (let index = 0; index < parts.length; index++) {
|
||||
crumbs.push('<span aria-hidden="true">/</span>');
|
||||
crumbs.push('<span class="breadcrumb-separator" aria-hidden="true">/</span>');
|
||||
if (index === parts.length - 1) {
|
||||
crumbs.push(`<span aria-current="page">${escapeHtml(parts[index])}</span>`);
|
||||
} else {
|
||||
@@ -170,26 +170,29 @@ function breadcrumbHtml(prefix) {
|
||||
function renderListing(page) {
|
||||
let rows = [];
|
||||
if (page.prefix) {
|
||||
rows.push(`<li><a class="entry parent" href="${parentPath(page.prefix)}"><span>Parent directory</span><span></span><span>Folder</span></a></li>`);
|
||||
rows.push(`<li><a class="listing-row parent-row" href="${parentPath(page.prefix)}"><span class="listing-cell entry"><span class="visually-hidden">Name: </span><span class="entry-icon entry-icon--parent" aria-hidden="true">↑</span><span class="entry-name">Parent directory</span></span><span class="listing-cell"></span><span class="listing-cell"></span></a></li>`);
|
||||
}
|
||||
for (let directory of page.directories) {
|
||||
rows.push(`<li><a class="entry" href="${directory.pathname}"><span>${escapeHtml(directory.name)}</span><span></span><span>Folder</span></a></li>`);
|
||||
rows.push(`<li><a class="listing-row" href="${directory.pathname}"><span class="listing-cell entry"><span class="visually-hidden">Name: </span><span class="entry-icon entry-icon--folder" aria-hidden="true"></span><span class="entry-name">${escapeHtml(directory.name)}</span></span><span class="listing-cell"></span><span class="listing-cell"><span class="visually-hidden">Type: </span><span class="type-label">Folder</span></span></a></li>`);
|
||||
}
|
||||
for (let file of page.files) {
|
||||
rows.push(`<li><a class="entry" href="${file.pathname}"><span>${escapeHtml(file.name)}</span><time datetime="${file.uploaded.toISOString()}">${formatDate(file.uploaded)}</time><span>${formatSize(file.size)}</span></a></li>`);
|
||||
rows.push(`<li><a class="listing-row" href="${file.pathname}"><span class="listing-cell entry"><span class="visually-hidden">Name: </span><span class="entry-icon entry-icon--file" aria-hidden="true"></span><span class="entry-name">${escapeHtml(file.name)}</span></span><span class="listing-cell"><span class="visually-hidden">Last modified: </span><time datetime="${file.uploaded.toISOString()}">${formatDate(file.uploaded)}</time></span><span class="listing-cell"><span class="visually-hidden">Size: </span>${formatSize(file.size)}</span></a></li>`);
|
||||
}
|
||||
if (!rows.length) {
|
||||
rows.push('<li class="empty">This directory is empty.</li>');
|
||||
rows.push('<li class="empty-state">This directory is empty.</li>');
|
||||
}
|
||||
|
||||
let jsonCursor = page.requestedCursor
|
||||
? `&cursor=${encodeURIComponent(page.requestedCursor)}`
|
||||
: '';
|
||||
let next = page.nextCursor
|
||||
? `<a class="next" rel="next" href="${page.pathname}?cursor=${encodeURIComponent(page.nextCursor)}">Next page →</a>`
|
||||
? `<a class="next-page" rel="next" href="${page.pathname}?cursor=${encodeURIComponent(page.nextCursor)}">Next page <span aria-hidden="true">→</span></a>`
|
||||
: '';
|
||||
let title = `Asset browser — ${page.pathname}`;
|
||||
let nonce = crypto.randomUUID().replaceAll('-', '');
|
||||
let folderCount = page.directories.length;
|
||||
let fileCount = page.files.length;
|
||||
let summary = `${folderCount} ${folderCount === 1 ? 'folder' : 'folders'} · ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`;
|
||||
|
||||
return {
|
||||
body: `<!doctype html>
|
||||
@@ -202,27 +205,295 @@ function renderListing(page) {
|
||||
<link rel="alternate" type="application/json" href="${page.pathname}?format=json${jsonCursor}">
|
||||
<title>${escapeHtml(title)}</title>
|
||||
<style nonce="${nonce}">
|
||||
:root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, sans-serif; }
|
||||
body { max-width: 64rem; margin: 0 auto; padding: 2rem; background: #f3f5f9; color: #18202f; }
|
||||
main { overflow: hidden; border: 1px solid #dce1ea; border-radius: 1rem; background: #fff; box-shadow: 0 1.5rem 4rem #261d521f; }
|
||||
header, footer { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem; }
|
||||
nav { display: flex; gap: .5rem; overflow: hidden; font: .9rem ui-monospace, monospace; white-space: nowrap; }
|
||||
nav a, .next { color: #6047d9; font-weight: 700; }
|
||||
ul { min-width: 34rem; margin: 0; padding: 0; list-style: none; }
|
||||
.entry { display: grid; grid-template-columns: minmax(0, 1fr) 14rem 7rem; gap: 1rem; padding: 1rem 1.25rem; border-top: 1px solid #dce1ea; color: inherit; text-decoration: none; }
|
||||
.entry:hover { background: #f7f5ff; }
|
||||
.parent { color: #657084; }
|
||||
.empty { padding: 2rem 1.25rem; border-top: 1px solid #dce1ea; color: #657084; text-align: center; }
|
||||
.summary { color: #657084; font-size: .85rem; white-space: nowrap; }
|
||||
footer:empty { display: none; }
|
||||
@media (prefers-color-scheme: dark) { body { background: #0b0e15; color: #f3f4f8; } main { border-color: #2b3240; background: #151923; } .entry { border-color: #2b3240; } .entry:hover { background: #1d2030; } nav a, .next { color: #a997ff; } }
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--background: #f3f5f9;
|
||||
--background-accent: #e8e4ff;
|
||||
--surface: rgba(255, 255, 255, 0.88);
|
||||
--surface-strong: #fff;
|
||||
--text: #18202f;
|
||||
--muted: #657084;
|
||||
--border: #dce1ea;
|
||||
--accent: #6047d9;
|
||||
--accent-soft: #eeeaff;
|
||||
--folder: #8a6cff;
|
||||
--file: #8290a6;
|
||||
--row-hover: #f7f5ff;
|
||||
--shadow: 0 24px 70px rgba(38, 29, 82, 0.12);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background:
|
||||
radial-gradient(circle at 15% 0%, var(--background-accent), transparent 34rem),
|
||||
var(--background);
|
||||
color: var(--text);
|
||||
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a { color: inherit; }
|
||||
|
||||
.shell {
|
||||
width: min(960px, calc(100% - 40px));
|
||||
margin: 0 auto;
|
||||
padding: 48px 0;
|
||||
}
|
||||
|
||||
.browser {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.browser-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
min-height: 68px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface-strong);
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.breadcrumbs a {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumbs a:hover { text-decoration: underline; }
|
||||
.breadcrumbs [aria-current="page"] { overflow: hidden; text-overflow: ellipsis; }
|
||||
.breadcrumb-separator { color: var(--muted); }
|
||||
|
||||
.summary { color: var(--muted); font-size: 0.82rem; }
|
||||
|
||||
.browser-meta {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.json-link {
|
||||
border-radius: 999px;
|
||||
padding: 3px 9px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 750;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.json-link:hover { text-decoration: underline; }
|
||||
.listing-wrap { overflow-x: auto; }
|
||||
|
||||
.listing {
|
||||
min-width: 520px;
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.listing-items { margin: 0; padding: 0; list-style: none; }
|
||||
|
||||
.listing-header, .listing-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 210px 110px;
|
||||
}
|
||||
|
||||
.listing-header > span {
|
||||
padding: 12px 20px;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.listing-row {
|
||||
min-height: 56px;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
transition: background-color 120ms ease;
|
||||
}
|
||||
|
||||
.listing-row:hover { background: var(--row-hover); }
|
||||
.listing-row:focus-visible { border-radius: 4px; outline: 2px solid var(--accent); outline-offset: -3px; }
|
||||
|
||||
.listing-cell {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
padding: 8px 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.entry {
|
||||
max-width: min(560px, 58vw);
|
||||
gap: 12px;
|
||||
color: var(--text);
|
||||
font-weight: 620;
|
||||
}
|
||||
|
||||
.entry-name { overflow: hidden; text-overflow: ellipsis; }
|
||||
.listing-row:hover .entry-name { color: var(--accent); text-decoration: underline; }
|
||||
|
||||
.entry-icon {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
flex: 0 0 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.entry-icon--folder {
|
||||
height: 13px;
|
||||
margin-top: 3px;
|
||||
border-radius: 3px;
|
||||
background: var(--folder);
|
||||
}
|
||||
|
||||
.entry-icon--folder::before {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: 1px;
|
||||
width: 8px;
|
||||
height: 5px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
background: var(--folder);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.entry-icon--file { border: 2px solid var(--file); border-radius: 3px; }
|
||||
|
||||
.entry-icon--file::after {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 3px;
|
||||
left: 2px;
|
||||
height: 2px;
|
||||
background: var(--file);
|
||||
box-shadow: 0 -4px 0 var(--file);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.entry-icon--parent {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 5px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.parent-row .entry { color: var(--muted); font-weight: 560; }
|
||||
|
||||
.type-label {
|
||||
display: inline-block;
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: grid;
|
||||
height: 120px;
|
||||
place-items: center;
|
||||
border-top: 1px solid var(--border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.browser-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--surface-strong);
|
||||
}
|
||||
|
||||
.browser-footer:empty { display: none; }
|
||||
.next-page { color: var(--accent); font-size: 0.86rem; font-weight: 700; text-decoration: none; }
|
||||
.next-page:hover { text-decoration: underline; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0b0e15;
|
||||
--background-accent: #201a43;
|
||||
--surface: rgba(20, 24, 35, 0.9);
|
||||
--surface-strong: #151923;
|
||||
--text: #f3f4f8;
|
||||
--muted: #9ba4b5;
|
||||
--border: #2b3240;
|
||||
--accent: #a997ff;
|
||||
--accent-soft: #292443;
|
||||
--folder: #9b87ff;
|
||||
--file: #8590a4;
|
||||
--row-hover: #1d2030;
|
||||
--shadow: 0 28px 80px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.shell { width: min(100% - 24px, 960px); padding: 24px 0; }
|
||||
.browser-header { align-items: flex-start; flex-direction: column; gap: 6px; }
|
||||
.browser-meta { width: 100%; justify-content: space-between; }
|
||||
.listing-header, .listing-row { grid-template-columns: minmax(0, 1fr) 190px 92px; }
|
||||
.listing-header > span, .listing-cell { padding-right: 14px; padding-left: 14px; }
|
||||
.entry { max-width: calc(100vw - 150px); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header><nav aria-label="Breadcrumb">${breadcrumbHtml(page.prefix)}</nav><span class="summary">${page.directories.length} folders · ${page.files.length} files · <a href="${page.pathname}?format=json${jsonCursor}">JSON</a></span></header>
|
||||
<ul aria-label="Directory entries">${rows.join('')}</ul>
|
||||
<footer>${next}</footer>
|
||||
<main class="shell">
|
||||
<section class="browser" aria-label="Directory contents">
|
||||
<div class="browser-header">
|
||||
<nav class="breadcrumbs" aria-label="Breadcrumb">${breadcrumbHtml(page.prefix)}</nav>
|
||||
<div class="browser-meta"><span class="summary">${summary}</span><a class="json-link" href="${page.pathname}?format=json${jsonCursor}">JSON</a></div>
|
||||
</div>
|
||||
<div class="listing-wrap">
|
||||
<div class="listing">
|
||||
<div class="listing-header"><span>Name</span><span>Last modified</span><span>Size</span></div>
|
||||
<ul class="listing-items" aria-label="Directory entries">${rows.join('')}</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="browser-footer">${next}</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>`,
|
||||
|
||||
@@ -37,6 +37,10 @@ describe('R2 directory index', () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('content-type')).toBe('text/html; charset=utf-8');
|
||||
expect(response.headers.get('cache-control')).toBe('no-store');
|
||||
expect(html).toContain('<main class="shell">');
|
||||
expect(html).toContain('class="entry-icon entry-icon--folder"');
|
||||
expect(html).toContain('class="entry-icon entry-icon--file"');
|
||||
expect(html).toContain('class="listing-header"');
|
||||
expect(html).toContain('href="/data/locale/"');
|
||||
expect(html).toContain('href="/data/schedules.json"');
|
||||
expect(html).not.toContain('__directory');
|
||||
|
||||
Reference in New Issue
Block a user