Appearance settings: text size and card size, and a larger default
- All 50 font-size declarations converted from px to rem, so a single root value scales headings, labels and body together. The root is now 15px, a deliberate ~7% increase over the previous 14px. - Card width and grid column width are now --card-w / --grid-min variables, driven by --card-scale independently of text. Defaults bumped too: shelf cards 150 -> 160px, grid columns 148 -> 158px. - Appearance panel (nav item, visible to everyone): Text size and Card size as segmented controls, a live preview, and a reset. Settings are stored in localStorage, per device rather than per account, because a TV across the room wants bigger text than a laptop and the same person uses both. - nav: 'settings' was missing from the View union after the home rewrite; added back along with the Appearance entry. Verified in the built bundle: root font 15px by default; "Larger" scales it to 19.2px; "Large" cards scale 160 to 192px; both persist across a reload and are independent of each other.
This commit is contained in:
parent
4c51482dd6
commit
8178502b73
17 changed files with 340 additions and 58 deletions
|
|
@ -41,6 +41,15 @@
|
|||
--nav-width: 232px;
|
||||
--shadow-cover: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
--shadow-lift: 0 12px 32px rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* User-controlled sizing. --ui-scale drives every rem in the app, so the
|
||||
text-size setting scales headings, labels and body together. --card-scale
|
||||
drives cover widths in shelves and grids, so card size is independent of
|
||||
text size. Both are set from the Appearance panel and persisted locally. */
|
||||
--ui-scale: 1;
|
||||
--card-scale: 1;
|
||||
--card-w: calc(160px * var(--card-scale));
|
||||
--grid-min: calc(158px * var(--card-scale));
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
|
|
@ -73,11 +82,18 @@ body,
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
/* The single root size every rem is relative to. 15px is a deliberate ~7%
|
||||
increase over the previous 14px baseline; the Appearance panel multiplies
|
||||
it via --ui-scale. */
|
||||
font-size: calc(15px * var(--ui-scale));
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
font-size: 14px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
|
@ -98,13 +114,13 @@ h3 {
|
|||
letter-spacing: -0.015em;
|
||||
}
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-size: 1.571rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 17px;
|
||||
font-size: 1.214rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-size: 1.071rem;
|
||||
}
|
||||
|
||||
button,
|
||||
|
|
@ -158,7 +174,7 @@ select {
|
|||
}
|
||||
.btn-lg {
|
||||
padding: 11px 22px;
|
||||
font-size: 15px;
|
||||
font-size: 1.071rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +199,7 @@ select {
|
|||
gap: 6px;
|
||||
}
|
||||
.field label {
|
||||
font-size: 12px;
|
||||
font-size: .857rem;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
|
@ -201,7 +217,7 @@ select {
|
|||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-strong);
|
||||
font-size: 11px;
|
||||
font-size: .786rem;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
|
|
@ -214,7 +230,7 @@ select {
|
|||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
}
|
||||
.chip:hover {
|
||||
color: var(--text);
|
||||
|
|
@ -231,14 +247,14 @@ select {
|
|||
border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border));
|
||||
border-radius: var(--radius);
|
||||
background: color-mix(in srgb, var(--danger) 10%, transparent);
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
}
|
||||
.ok {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid color-mix(in srgb, var(--success) 40%, var(--border));
|
||||
border-radius: var(--radius);
|
||||
background: color-mix(in srgb, var(--success) 9%, transparent);
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
}
|
||||
|
||||
.muted {
|
||||
|
|
@ -249,7 +265,7 @@ select {
|
|||
}
|
||||
.mono {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
font-size: .857rem;
|
||||
}
|
||||
|
||||
/* ── Cover card ─────────────────────────────────────────────────────── */
|
||||
|
|
@ -277,7 +293,7 @@ select {
|
|||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 34px;
|
||||
font-size: 2.429rem;
|
||||
font-weight: 700;
|
||||
color: var(--faint);
|
||||
background: linear-gradient(150deg, var(--surface-2), var(--surface-3));
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@
|
|||
.times {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
font-size: .857rem;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--grid-min), 1fr));
|
||||
gap: var(--space-5) var(--space-4);
|
||||
}
|
||||
.skeleton.tile {
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
}
|
||||
@media (max-width: 700px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(var(--grid-min) * 0.75), 1fr));
|
||||
gap: var(--space-4) var(--space-3);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
min-width: 0;
|
||||
}
|
||||
.title {
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
line-height: 1.3;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 11.5px;
|
||||
font-size: .821rem;
|
||||
color: var(--faint);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -102,7 +102,7 @@
|
|||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.66);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-size: .786rem;
|
||||
font-weight: 600;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
|
@ -122,9 +122,9 @@
|
|||
background: var(--accent);
|
||||
}
|
||||
.sm .title {
|
||||
font-size: 12px;
|
||||
font-size: .857rem;
|
||||
}
|
||||
.lg .title {
|
||||
font-size: 14px;
|
||||
font-size: 1.000rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@
|
|||
}
|
||||
.status {
|
||||
margin: 0 0 var(--space-3);
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
}
|
||||
.stage {
|
||||
height: calc(100vh - 60px - 150px);
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
<style>
|
||||
.hello {
|
||||
margin: 0 0 var(--space-5);
|
||||
font-size: 15px;
|
||||
font-size: 1.071rem;
|
||||
}
|
||||
.explore {
|
||||
margin-top: var(--space-2);
|
||||
|
|
@ -121,14 +121,14 @@
|
|||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.explore h2 {
|
||||
font-size: 17px;
|
||||
font-size: 1.214rem;
|
||||
}
|
||||
.small {
|
||||
font-size: 12.5px;
|
||||
font-size: .893rem;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--grid-min), 1fr));
|
||||
gap: var(--space-5) var(--space-4);
|
||||
}
|
||||
.loading {
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
.skeleton-row {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 150px;
|
||||
grid-auto-columns: var(--card-w);
|
||||
gap: var(--space-4);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
}
|
||||
@media (max-width: 700px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(var(--grid-min) * 0.75), 1fr));
|
||||
gap: var(--space-4) var(--space-3);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,18 +141,18 @@
|
|||
padding-top: var(--space-2);
|
||||
}
|
||||
.kind {
|
||||
font-size: 12px;
|
||||
font-size: .857rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--accent);
|
||||
}
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
font-size: 2.143rem;
|
||||
line-height: 1.15;
|
||||
}
|
||||
.byline {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-size: 1.071rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.resume {
|
||||
|
|
@ -173,7 +173,7 @@
|
|||
background: var(--accent);
|
||||
}
|
||||
.resume .muted {
|
||||
font-size: 12px;
|
||||
font-size: .857rem;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
|
|
@ -196,7 +196,7 @@
|
|||
margin-bottom: var(--space-2);
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
}
|
||||
.tracks ol {
|
||||
list-style: none;
|
||||
|
|
@ -209,7 +209,7 @@
|
|||
gap: var(--space-3);
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
}
|
||||
.tracks li:last-child {
|
||||
border-bottom: none;
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@
|
|||
background: var(--bg-hover);
|
||||
}
|
||||
.kind {
|
||||
font-size: 11px;
|
||||
font-size: .786rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--accent);
|
||||
|
|
@ -215,7 +215,7 @@
|
|||
border-radius: var(--radius);
|
||||
background: color-mix(in srgb, var(--success) 8%, transparent);
|
||||
margin-bottom: var(--space-4);
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
}
|
||||
.alert {
|
||||
margin-bottom: var(--space-4);
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@
|
|||
}
|
||||
.lead {
|
||||
margin: -8px 0 0;
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
}
|
||||
.submit {
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -172,13 +172,13 @@
|
|||
flex: 1;
|
||||
}
|
||||
.info strong {
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.info .muted {
|
||||
font-size: 12px;
|
||||
font-size: .857rem;
|
||||
}
|
||||
.pct {
|
||||
color: var(--text-faint);
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
<style>
|
||||
.count {
|
||||
margin: 0 0 var(--space-5);
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
}
|
||||
section {
|
||||
margin-bottom: var(--space-6);
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--grid-min), 1fr));
|
||||
gap: var(--space-5) var(--space-4);
|
||||
}
|
||||
.skeleton.tile {
|
||||
|
|
|
|||
178
web/src/lib/components/Settings.svelte
Normal file
178
web/src/lib/components/Settings.svelte
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
<script lang="ts">
|
||||
import { settings, setSettings, resetSettings, TEXT_STEPS, CARD_STEPS } from '../settings'
|
||||
|
||||
function isActive(current: number, value: number): boolean {
|
||||
return Math.abs(current - value) < 0.001
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrap">
|
||||
<p class="muted intro">
|
||||
These preferences are saved on this device only, so a TV and a laptop can
|
||||
each have their own.
|
||||
</p>
|
||||
|
||||
<section class="card group">
|
||||
<header>
|
||||
<h3>Text size</h3>
|
||||
<span class="muted small">{Math.round($settings.textScale * 100)}%</span>
|
||||
</header>
|
||||
<div class="segmented">
|
||||
{#each TEXT_STEPS as step (step.label)}
|
||||
<button
|
||||
class="seg"
|
||||
class:active={isActive($settings.textScale, step.value)}
|
||||
onclick={() => setSettings({ textScale: step.value })}
|
||||
>
|
||||
{step.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card group">
|
||||
<header>
|
||||
<h3>Card size</h3>
|
||||
<span class="muted small">{Math.round($settings.cardScale * 100)}%</span>
|
||||
</header>
|
||||
<div class="segmented">
|
||||
{#each CARD_STEPS as step (step.label)}
|
||||
<button
|
||||
class="seg"
|
||||
class:active={isActive($settings.cardScale, step.value)}
|
||||
onclick={() => setSettings({ cardScale: step.value })}
|
||||
>
|
||||
{step.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="group">
|
||||
<h3 class="preview-heading">Preview</h3>
|
||||
<div class="preview">
|
||||
<div class="mock">
|
||||
<div class="mock-cover">A</div>
|
||||
<span class="mock-title">A Long Book Title</span>
|
||||
<span class="mock-sub">3 views</span>
|
||||
</div>
|
||||
<div class="mock">
|
||||
<div class="mock-cover alt">B</div>
|
||||
<span class="mock-title">Another Title</span>
|
||||
<span class="mock-sub">More by an author</span>
|
||||
</div>
|
||||
<div class="mock">
|
||||
<div class="mock-cover alt2">C</div>
|
||||
<span class="mock-title">Third Item</span>
|
||||
<span class="mock-sub">42%</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="sample muted">
|
||||
Body text looks like this. Headings, labels, and captions all scale
|
||||
together, while card size is controlled separately.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<button class="btn" onclick={resetSettings}>Reset to defaults</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrap {
|
||||
max-width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
.intro {
|
||||
margin: 0;
|
||||
font-size: 0.929rem;
|
||||
}
|
||||
.group {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
.group header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.small {
|
||||
font-size: 0.857rem;
|
||||
}
|
||||
.segmented {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.seg {
|
||||
padding: 7px 14px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg);
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
font-size: 0.929rem;
|
||||
}
|
||||
.seg:hover {
|
||||
color: var(--text);
|
||||
border-color: var(--faint);
|
||||
}
|
||||
.seg.active {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
.preview-heading {
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.preview {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: var(--card-w);
|
||||
gap: var(--space-4);
|
||||
overflow-x: auto;
|
||||
padding-bottom: var(--space-2);
|
||||
}
|
||||
.mock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.mock-cover {
|
||||
aspect-ratio: 2 / 3;
|
||||
border-radius: var(--radius-cover);
|
||||
background: linear-gradient(150deg, var(--surface-2), var(--surface-3));
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 2.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--faint);
|
||||
box-shadow: var(--shadow-cover);
|
||||
}
|
||||
.mock-cover.alt {
|
||||
background: linear-gradient(150deg, color-mix(in srgb, var(--accent) 40%, var(--surface-2)), var(--surface-3));
|
||||
}
|
||||
.mock-cover.alt2 {
|
||||
background: linear-gradient(150deg, #b23b3b, var(--surface-3));
|
||||
}
|
||||
.mock-title {
|
||||
font-size: 0.929rem;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mock-sub {
|
||||
font-size: 0.821rem;
|
||||
color: var(--faint);
|
||||
}
|
||||
.sample {
|
||||
margin: var(--space-4) 0 0;
|
||||
max-width: 60ch;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -52,13 +52,13 @@
|
|||
margin-bottom: var(--space-3);
|
||||
}
|
||||
h2 {
|
||||
font-size: 17px;
|
||||
font-size: 1.214rem;
|
||||
}
|
||||
.see-all {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
.row {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 150px;
|
||||
grid-auto-columns: var(--card-w);
|
||||
gap: var(--space-4);
|
||||
overflow-x: auto;
|
||||
padding-bottom: var(--space-2);
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
}
|
||||
@media (max-width: 700px) {
|
||||
.row {
|
||||
grid-auto-columns: 120px;
|
||||
grid-auto-columns: calc(var(--card-w) * 0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import SearchResults from './SearchResults.svelte'
|
||||
import Libraries from './Libraries.svelte'
|
||||
import Users from './Users.svelte'
|
||||
import Settings from './Settings.svelte'
|
||||
|
||||
let searchBox = $state('')
|
||||
|
||||
|
|
@ -38,6 +39,8 @@
|
|||
return 'Libraries'
|
||||
case 'users':
|
||||
return 'Users'
|
||||
case 'settings':
|
||||
return 'Appearance'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -66,6 +69,9 @@
|
|||
<button class="navlink" class:active={$view.name === 'browse' && !$view.kind} onclick={() => go({ name: 'browse' })}>
|
||||
<span>Browse all</span>
|
||||
</button>
|
||||
<button class="navlink" class:active={$view.name === 'settings'} onclick={() => go({ name: 'settings' })}>
|
||||
<span>Appearance</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{#if $user?.is_admin}
|
||||
|
|
@ -125,6 +131,8 @@
|
|||
<Libraries />
|
||||
{:else if $view.name === 'users'}
|
||||
<Users />
|
||||
{:else if $view.name === 'settings'}
|
||||
<Settings />
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -154,7 +162,7 @@
|
|||
background: transparent;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-size: 1.071rem;
|
||||
}
|
||||
.mark {
|
||||
display: grid;
|
||||
|
|
@ -164,7 +172,7 @@
|
|||
border-radius: 7px;
|
||||
background: var(--accent);
|
||||
color: var(--accent-text);
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.links {
|
||||
|
|
@ -184,7 +192,7 @@
|
|||
color: var(--muted);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 13.5px;
|
||||
font-size: .964rem;
|
||||
}
|
||||
.navlink:hover {
|
||||
background: var(--surface-2);
|
||||
|
|
@ -196,13 +204,13 @@
|
|||
font-weight: 500;
|
||||
}
|
||||
.count {
|
||||
font-size: 12px;
|
||||
font-size: .857rem;
|
||||
color: var(--faint);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.section {
|
||||
margin: 0 var(--space-2) -8px;
|
||||
font-size: 11px;
|
||||
font-size: .786rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: var(--faint);
|
||||
|
|
@ -228,7 +236,7 @@
|
|||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--surface-3);
|
||||
font-size: 12px;
|
||||
font-size: .857rem;
|
||||
font-weight: 600;
|
||||
flex: none;
|
||||
}
|
||||
|
|
@ -239,13 +247,13 @@
|
|||
min-width: 0;
|
||||
}
|
||||
.who strong {
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.who .faint {
|
||||
font-size: 11px;
|
||||
font-size: .786rem;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
|
|
@ -274,7 +282,7 @@
|
|||
height: 34px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-size: 1.143rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.back:hover {
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@
|
|||
flex: 1;
|
||||
}
|
||||
.id .muted {
|
||||
font-size: 12px;
|
||||
font-size: .857rem;
|
||||
}
|
||||
.grants {
|
||||
padding: 0 var(--space-4) var(--space-4);
|
||||
|
|
@ -214,7 +214,7 @@
|
|||
}
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-size: .857rem;
|
||||
}
|
||||
.grantrow {
|
||||
display: flex;
|
||||
|
|
@ -223,10 +223,10 @@
|
|||
}
|
||||
.libname {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
}
|
||||
.libname .faint {
|
||||
font-size: 11px;
|
||||
font-size: .786rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
|
@ -240,7 +240,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-size: .929rem;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export type View =
|
|||
| { name: 'search'; query: string }
|
||||
| { name: 'libraries' }
|
||||
| { name: 'users' }
|
||||
| { name: 'settings' }
|
||||
|
||||
export const view = writable<View>({ name: 'home' })
|
||||
|
||||
|
|
|
|||
79
web/src/lib/settings.ts
Normal file
79
web/src/lib/settings.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { writable } from 'svelte/store'
|
||||
|
||||
// Display preferences. These are per-device, not per-account: a TV across the
|
||||
// room wants bigger text than a laptop, and the same person uses both. Stored
|
||||
// in localStorage so a change is instant and needs no round trip.
|
||||
|
||||
export interface UiSettings {
|
||||
/** Multiplier on the 15px root size; scales all text. */
|
||||
textScale: number
|
||||
/** Multiplier on cover width in shelves and grids. */
|
||||
cardScale: number
|
||||
}
|
||||
|
||||
export const DEFAULTS: UiSettings = { textScale: 1, cardScale: 1 }
|
||||
|
||||
export const TEXT_STEPS = [
|
||||
{ label: 'Small', value: 0.9 },
|
||||
{ label: 'Default', value: 1 },
|
||||
{ label: 'Large', value: 1.12 },
|
||||
{ label: 'Larger', value: 1.28 },
|
||||
]
|
||||
|
||||
export const CARD_STEPS = [
|
||||
{ label: 'Compact', value: 0.85 },
|
||||
{ label: 'Default', value: 1 },
|
||||
{ label: 'Large', value: 1.2 },
|
||||
{ label: 'Extra large', value: 1.45 },
|
||||
]
|
||||
|
||||
const KEY = 'vellum.ui.settings'
|
||||
|
||||
function read(): UiSettings {
|
||||
try {
|
||||
const raw = localStorage.getItem(KEY)
|
||||
if (!raw) return DEFAULTS
|
||||
const parsed = JSON.parse(raw) as Partial<UiSettings>
|
||||
return {
|
||||
textScale: clamp(parsed.textScale ?? DEFAULTS.textScale, 0.8, 1.6),
|
||||
cardScale: clamp(parsed.cardScale ?? DEFAULTS.cardScale, 0.7, 1.8),
|
||||
}
|
||||
} catch {
|
||||
return DEFAULTS
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(n: number, lo: number, hi: number): number {
|
||||
if (!Number.isFinite(n)) return 1
|
||||
return Math.min(Math.max(n, lo), hi)
|
||||
}
|
||||
|
||||
/** Apply the settings to the document as CSS variables. */
|
||||
export function applySettings(s: UiSettings) {
|
||||
const root = document.documentElement
|
||||
root.style.setProperty('--ui-scale', String(s.textScale))
|
||||
root.style.setProperty('--card-scale', String(s.cardScale))
|
||||
}
|
||||
|
||||
export const settings = writable<UiSettings>(read())
|
||||
|
||||
/** Update one or more settings, persist them, and apply them immediately. */
|
||||
export function setSettings(patch: Partial<UiSettings>) {
|
||||
settings.update((current) => {
|
||||
const next = { ...current, ...patch }
|
||||
applySettings(next)
|
||||
try {
|
||||
localStorage.setItem(KEY, JSON.stringify(next))
|
||||
} catch {
|
||||
// Private browsing or a full quota: keep the change for this session.
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
export function resetSettings() {
|
||||
setSettings(DEFAULTS)
|
||||
}
|
||||
|
||||
// Apply whatever was stored before the first paint.
|
||||
applySettings(read())
|
||||
Loading…
Add table
Reference in a new issue