[web] use a dashboard type table and compact sign-in sizing #195
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@luzzle/web": patch
|
||||
---
|
||||
|
||||
Show dashboard piece types in a compact table while retaining their names, counts, browse links and type-specific create actions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@luzzle/web": patch
|
||||
---
|
||||
|
||||
Use compact sign-in typography, card sizing and form spacing while preserving themed colors and fonts, 16px credential inputs, 44px controls and existing authentication behavior. Leave public-site typography unchanged.
|
||||
@@ -120,24 +120,35 @@
|
||||
|
||||
{#if data.pieceStats.byType.length}
|
||||
<section class="dashboard-section">
|
||||
<h2 class="section-header">pieces by type</h2>
|
||||
<div class="stats-row">
|
||||
{#each data.pieceStats.byType as { type, count } (type)}
|
||||
<div class="stat-card-wrapper">
|
||||
<a class="stat-card" href={resolve(`pieces/${type}`)}>
|
||||
<span class="stat-number">{count.toLocaleString()}</span>
|
||||
<span class="stat-label">{type}</span>
|
||||
</a>
|
||||
<a
|
||||
class="quick-add"
|
||||
href={resolve(`admin/pieces/create?type=${type}`)}
|
||||
aria-label="add {type}"
|
||||
title="add {type}"
|
||||
>
|
||||
<PlusIcon style="width: 1em; height: 1em;" />
|
||||
</a>
|
||||
</div>
|
||||
{/each}
|
||||
<h2 id="piece-types-title" class="section-header">pieces by type</h2>
|
||||
<div class="table-card">
|
||||
<table class="type-table" aria-labelledby="piece-types-title">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">type</th>
|
||||
<th scope="col" class="type-count">pieces</th>
|
||||
<th scope="col" class="type-actions">action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.pieceStats.byType as { type, count } (type)}
|
||||
<tr>
|
||||
<th scope="row"><a href={resolve(`pieces/${type}`)} title={type}>{type}</a></th>
|
||||
<td class="type-count">{count.toLocaleString()}</td>
|
||||
<td class="type-actions">
|
||||
<a
|
||||
class="type-add"
|
||||
href={resolve(`admin/pieces/create?type=${type}`)}
|
||||
aria-label="add {type}"
|
||||
title="add {type}"
|
||||
>
|
||||
<PlusIcon aria-hidden="true" style="width: 1em; height: 1em;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
@@ -175,6 +186,11 @@
|
||||
|
||||
<style>
|
||||
section.dashboard {
|
||||
--middle-column-width: 8rem;
|
||||
--last-column-width: 9rem;
|
||||
--column-gap: var(--space-3);
|
||||
--row-block-padding: var(--space-3);
|
||||
--row-inline-padding: var(--space-4);
|
||||
margin: var(--space-4);
|
||||
margin-bottom: var(--space-8);
|
||||
margin-left: auto;
|
||||
@@ -272,13 +288,92 @@
|
||||
border-radius: var(--radius-small);
|
||||
}
|
||||
|
||||
.type-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.type-table th,
|
||||
.type-table td {
|
||||
padding: var(--row-block-padding) var(--row-inline-padding);
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid var(--color-outline-variant);
|
||||
}
|
||||
|
||||
.type-table th {
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.type-table thead {
|
||||
background: var(--color-surface-container-high);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-on-surface-variant);
|
||||
}
|
||||
|
||||
.type-table tbody tr:last-child > * {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.type-table tbody tr:hover {
|
||||
background: var(--color-surface-container-low);
|
||||
}
|
||||
|
||||
.type-table a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.type-table th a {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.type-table a:hover {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.type-table a:focus-visible {
|
||||
outline: auto;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Table cells include padding; the recent grid sizes its content tracks separately. */
|
||||
.type-count {
|
||||
width: calc(var(--middle-column-width) + var(--column-gap));
|
||||
font-size: var(--font-size-xs);
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.type-actions {
|
||||
width: calc(var(--last-column-width) + 2 * var(--row-inline-padding));
|
||||
}
|
||||
|
||||
.type-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: var(--radius-small);
|
||||
}
|
||||
|
||||
.recent-header,
|
||||
.recent-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 8rem 9rem;
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: 1fr var(--middle-column-width) var(--last-column-width);
|
||||
gap: var(--column-gap);
|
||||
align-items: center;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
padding: var(--row-block-padding) var(--row-inline-padding);
|
||||
}
|
||||
|
||||
.recent-header {
|
||||
@@ -337,6 +432,13 @@
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.type-table {
|
||||
--middle-column-width: 5rem;
|
||||
--last-column-width: 3rem;
|
||||
--column-gap: var(--space-2);
|
||||
--row-inline-padding: var(--space-2);
|
||||
}
|
||||
|
||||
.recent-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ComponentProps } from 'svelte'
|
||||
import type { WebPieces } from '@luzzle/web.db'
|
||||
import { resolve } from '$app/paths'
|
||||
import { makePublicConfig } from '#lib/server/config.fixture.js'
|
||||
import type { PieceStats } from '#lib/server/dashboardStats.js'
|
||||
import Page from './+page.svelte'
|
||||
|
||||
vi.mock('$app/paths', () => ({
|
||||
@@ -33,7 +34,7 @@ beforeEach(() => {
|
||||
})
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
function html(pieces = [piece]) {
|
||||
function html(pieces = [piece], pieceStats: PieceStats = { total: pieces.length, byType: [] }) {
|
||||
const config = makePublicConfig({
|
||||
url: { app: 'http://localhost', app_assets: '', luzzle_assets: '' }
|
||||
})
|
||||
@@ -44,8 +45,8 @@ function html(pieces = [piece]) {
|
||||
pieceTypes: ['books'],
|
||||
config,
|
||||
meta: { title: 'Dashboard' },
|
||||
pieceStats: { total: pieces.length, byType: [] },
|
||||
fileCount: pieces.length,
|
||||
pieceStats,
|
||||
fileCount: pieceStats.total,
|
||||
recentlyEditedPieces: pieces,
|
||||
lastPublish: null,
|
||||
setup: {
|
||||
@@ -60,6 +61,29 @@ function html(pieces = [piece]) {
|
||||
}).body.replace(/<!--[\s\S]*?-->/g, '')
|
||||
}
|
||||
|
||||
test('type rows retain their name, count, browse link and type-specific create link', () => {
|
||||
const stats = {
|
||||
total: 15,
|
||||
byType: [
|
||||
{ type: 'books', count: 12 },
|
||||
{ type: 'films', count: 3 }
|
||||
]
|
||||
}
|
||||
const output = html([], stats)
|
||||
const body = output.match(/<tbody\b[^>]*>([\s\S]*?)<\/tbody>/)?.[1] ?? ''
|
||||
const rows = [...body.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/g)].map((match) => match[1])
|
||||
|
||||
expect(rows).toHaveLength(stats.byType.length)
|
||||
for (const [index, { type, count }] of stats.byType.entries()) {
|
||||
const row = rows[index]
|
||||
expect(row).toContain(`href="/pieces/${type}"`)
|
||||
expect(row).toContain(`>${type}</a>`)
|
||||
expect(row).toMatch(new RegExp(`<td\\b[^>]*>\\s*${count.toLocaleString()}\\s*</td>`))
|
||||
expect(row).toContain(`href="/admin/pieces/create?type=${type}"`)
|
||||
expect(row).toContain(`aria-label="add ${type}"`)
|
||||
}
|
||||
})
|
||||
|
||||
test('each recent row is one native link containing title, type, and updated time', () => {
|
||||
const output = html([piece, { ...piece, id: 'book-2', file_path: 'books/another.books.md' }])
|
||||
const list = output.match(/<ul\b[^>]*class="recent-list[^"]*"[^>]*>([\s\S]*?)<\/ul>/)?.[1]
|
||||
|
||||
@@ -33,18 +33,18 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-3) var(--space-5);
|
||||
padding: 8px 16px;
|
||||
color: var(--color-on-surface-variant);
|
||||
}
|
||||
|
||||
.signin-tools a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
gap: 8px;
|
||||
min-height: 44px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-small);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.signin-tools a:hover {
|
||||
@@ -61,7 +61,7 @@
|
||||
border-radius: var(--radius-small);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-size: var(--font-size-large);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -77,13 +77,13 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
padding: var(--space-6) var(--space-4) var(--space-8);
|
||||
padding: 24px 16px 32px;
|
||||
}
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
main {
|
||||
align-items: flex-start;
|
||||
padding-block-start: var(--space-5);
|
||||
padding-block-start: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -153,11 +153,13 @@
|
||||
<style>
|
||||
.signin-card {
|
||||
width: 100%;
|
||||
max-width: 25rem;
|
||||
max-width: 400px;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
padding: var(--space-8);
|
||||
gap: 20px;
|
||||
padding: 32px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
background: var(--color-surface-container-low);
|
||||
border: 1px solid var(--color-outline-variant);
|
||||
border-radius: var(--radius-large);
|
||||
@@ -168,7 +170,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -178,7 +180,7 @@
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-xl);
|
||||
font-size: 24px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
@@ -189,32 +191,32 @@
|
||||
.javascript-notice {
|
||||
margin: 0;
|
||||
color: var(--color-on-surface-variant);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.signin-form {
|
||||
display: grid;
|
||||
gap: var(--space-5);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: var(--font-size-small);
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 50px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
min-height: 44px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-outline);
|
||||
border-radius: var(--radius-small);
|
||||
background: var(--color-surface-container-highest);
|
||||
@@ -239,25 +241,25 @@
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 50px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
min-height: 44px;
|
||||
padding: 10px 12px;
|
||||
text-transform: none;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: var(--font-size-normal);
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-error);
|
||||
border-radius: var(--radius-small);
|
||||
background: var(--color-error-container);
|
||||
color: var(--color-on-error-container);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -274,7 +276,7 @@
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
.signin-card {
|
||||
padding: var(--space-4);
|
||||
padding: 20px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
|
||||
Reference in new issue
Block a user