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;
|
--nav-width: 232px;
|
||||||
--shadow-cover: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
|
--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);
|
--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) {
|
@media (prefers-color-scheme: light) {
|
||||||
|
|
@ -73,11 +82,18 @@ body,
|
||||||
margin: 0;
|
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 {
|
body {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
font-size: 14px;
|
font-size: 1rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
@ -98,13 +114,13 @@ h3 {
|
||||||
letter-spacing: -0.015em;
|
letter-spacing: -0.015em;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 22px;
|
font-size: 1.571rem;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 17px;
|
font-size: 1.214rem;
|
||||||
}
|
}
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 15px;
|
font-size: 1.071rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
|
|
@ -158,7 +174,7 @@ select {
|
||||||
}
|
}
|
||||||
.btn-lg {
|
.btn-lg {
|
||||||
padding: 11px 22px;
|
padding: 11px 22px;
|
||||||
font-size: 15px;
|
font-size: 1.071rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,7 +199,7 @@ select {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.field label {
|
.field label {
|
||||||
font-size: 12px;
|
font-size: .857rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +217,7 @@ select {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
font-size: 11px;
|
font-size: .786rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
|
@ -214,7 +230,7 @@ select {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
}
|
}
|
||||||
.chip:hover {
|
.chip:hover {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|
@ -231,14 +247,14 @@ select {
|
||||||
border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border));
|
border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border));
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: color-mix(in srgb, var(--danger) 10%, transparent);
|
background: color-mix(in srgb, var(--danger) 10%, transparent);
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
}
|
}
|
||||||
.ok {
|
.ok {
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border: 1px solid color-mix(in srgb, var(--success) 40%, var(--border));
|
border: 1px solid color-mix(in srgb, var(--success) 40%, var(--border));
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: color-mix(in srgb, var(--success) 9%, transparent);
|
background: color-mix(in srgb, var(--success) 9%, transparent);
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted {
|
.muted {
|
||||||
|
|
@ -249,7 +265,7 @@ select {
|
||||||
}
|
}
|
||||||
.mono {
|
.mono {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 12px;
|
font-size: .857rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Cover card ─────────────────────────────────────────────────────── */
|
/* ── Cover card ─────────────────────────────────────────────────────── */
|
||||||
|
|
@ -277,7 +293,7 @@ select {
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
font-size: 34px;
|
font-size: 2.429rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
background: linear-gradient(150deg, var(--surface-2), var(--surface-3));
|
background: linear-gradient(150deg, var(--surface-2), var(--surface-3));
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@
|
||||||
.times {
|
.times {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 12px;
|
font-size: .857rem;
|
||||||
}
|
}
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
}
|
}
|
||||||
.grid {
|
.grid {
|
||||||
display: 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);
|
gap: var(--space-5) var(--space-4);
|
||||||
}
|
}
|
||||||
.skeleton.tile {
|
.skeleton.tile {
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
}
|
}
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.grid {
|
.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);
|
gap: var(--space-4) var(--space-3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -88,7 +88,7 @@
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 11.5px;
|
font-size: .821rem;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
@ -102,7 +102,7 @@
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(0, 0, 0, 0.66);
|
background: rgba(0, 0, 0, 0.66);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 11px;
|
font-size: .786rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
@ -122,9 +122,9 @@
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
.sm .title {
|
.sm .title {
|
||||||
font-size: 12px;
|
font-size: .857rem;
|
||||||
}
|
}
|
||||||
.lg .title {
|
.lg .title {
|
||||||
font-size: 14px;
|
font-size: 1.000rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@
|
||||||
}
|
}
|
||||||
.status {
|
.status {
|
||||||
margin: 0 0 var(--space-3);
|
margin: 0 0 var(--space-3);
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
}
|
}
|
||||||
.stage {
|
.stage {
|
||||||
height: calc(100vh - 60px - 150px);
|
height: calc(100vh - 60px - 150px);
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@
|
||||||
<style>
|
<style>
|
||||||
.hello {
|
.hello {
|
||||||
margin: 0 0 var(--space-5);
|
margin: 0 0 var(--space-5);
|
||||||
font-size: 15px;
|
font-size: 1.071rem;
|
||||||
}
|
}
|
||||||
.explore {
|
.explore {
|
||||||
margin-top: var(--space-2);
|
margin-top: var(--space-2);
|
||||||
|
|
@ -121,14 +121,14 @@
|
||||||
margin-bottom: var(--space-4);
|
margin-bottom: var(--space-4);
|
||||||
}
|
}
|
||||||
.explore h2 {
|
.explore h2 {
|
||||||
font-size: 17px;
|
font-size: 1.214rem;
|
||||||
}
|
}
|
||||||
.small {
|
.small {
|
||||||
font-size: 12.5px;
|
font-size: .893rem;
|
||||||
}
|
}
|
||||||
.grid {
|
.grid {
|
||||||
display: 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);
|
gap: var(--space-5) var(--space-4);
|
||||||
}
|
}
|
||||||
.loading {
|
.loading {
|
||||||
|
|
@ -144,7 +144,7 @@
|
||||||
.skeleton-row {
|
.skeleton-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-auto-columns: 150px;
|
grid-auto-columns: var(--card-w);
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
}
|
}
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.grid {
|
.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);
|
gap: var(--space-4) var(--space-3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,18 +141,18 @@
|
||||||
padding-top: var(--space-2);
|
padding-top: var(--space-2);
|
||||||
}
|
}
|
||||||
.kind {
|
.kind {
|
||||||
font-size: 12px;
|
font-size: .857rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 30px;
|
font-size: 2.143rem;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
}
|
}
|
||||||
.byline {
|
.byline {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 15px;
|
font-size: 1.071rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.resume {
|
.resume {
|
||||||
|
|
@ -173,7 +173,7 @@
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
.resume .muted {
|
.resume .muted {
|
||||||
font-size: 12px;
|
font-size: .857rem;
|
||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -196,7 +196,7 @@
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
}
|
}
|
||||||
.tracks ol {
|
.tracks ol {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
@ -209,7 +209,7 @@
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
}
|
}
|
||||||
.tracks li:last-child {
|
.tracks li:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@
|
||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
.kind {
|
.kind {
|
||||||
font-size: 11px;
|
font-size: .786rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|
@ -215,7 +215,7 @@
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: color-mix(in srgb, var(--success) 8%, transparent);
|
background: color-mix(in srgb, var(--success) 8%, transparent);
|
||||||
margin-bottom: var(--space-4);
|
margin-bottom: var(--space-4);
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
}
|
}
|
||||||
.alert {
|
.alert {
|
||||||
margin-bottom: var(--space-4);
|
margin-bottom: var(--space-4);
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
}
|
}
|
||||||
.lead {
|
.lead {
|
||||||
margin: -8px 0 0;
|
margin: -8px 0 0;
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
}
|
}
|
||||||
.submit {
|
.submit {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
||||||
|
|
@ -172,13 +172,13 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.info strong {
|
.info strong {
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.info .muted {
|
.info .muted {
|
||||||
font-size: 12px;
|
font-size: .857rem;
|
||||||
}
|
}
|
||||||
.pct {
|
.pct {
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
<style>
|
<style>
|
||||||
.count {
|
.count {
|
||||||
margin: 0 0 var(--space-5);
|
margin: 0 0 var(--space-5);
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
margin-bottom: var(--space-6);
|
margin-bottom: var(--space-6);
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
}
|
}
|
||||||
.grid {
|
.grid {
|
||||||
display: 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);
|
gap: var(--space-5) var(--space-4);
|
||||||
}
|
}
|
||||||
.skeleton.tile {
|
.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);
|
margin-bottom: var(--space-3);
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 17px;
|
font-size: 1.214rem;
|
||||||
}
|
}
|
||||||
.see-all {
|
.see-all {
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
.row {
|
.row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-auto-columns: 150px;
|
grid-auto-columns: var(--card-w);
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding-bottom: var(--space-2);
|
padding-bottom: var(--space-2);
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
}
|
}
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.row {
|
.row {
|
||||||
grid-auto-columns: 120px;
|
grid-auto-columns: calc(var(--card-w) * 0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import SearchResults from './SearchResults.svelte'
|
import SearchResults from './SearchResults.svelte'
|
||||||
import Libraries from './Libraries.svelte'
|
import Libraries from './Libraries.svelte'
|
||||||
import Users from './Users.svelte'
|
import Users from './Users.svelte'
|
||||||
|
import Settings from './Settings.svelte'
|
||||||
|
|
||||||
let searchBox = $state('')
|
let searchBox = $state('')
|
||||||
|
|
||||||
|
|
@ -38,6 +39,8 @@
|
||||||
return 'Libraries'
|
return 'Libraries'
|
||||||
case 'users':
|
case 'users':
|
||||||
return 'Users'
|
return 'Users'
|
||||||
|
case 'settings':
|
||||||
|
return 'Appearance'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -66,6 +69,9 @@
|
||||||
<button class="navlink" class:active={$view.name === 'browse' && !$view.kind} onclick={() => go({ name: 'browse' })}>
|
<button class="navlink" class:active={$view.name === 'browse' && !$view.kind} onclick={() => go({ name: 'browse' })}>
|
||||||
<span>Browse all</span>
|
<span>Browse all</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="navlink" class:active={$view.name === 'settings'} onclick={() => go({ name: 'settings' })}>
|
||||||
|
<span>Appearance</span>
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{#if $user?.is_admin}
|
{#if $user?.is_admin}
|
||||||
|
|
@ -125,6 +131,8 @@
|
||||||
<Libraries />
|
<Libraries />
|
||||||
{:else if $view.name === 'users'}
|
{:else if $view.name === 'users'}
|
||||||
<Users />
|
<Users />
|
||||||
|
{:else if $view.name === 'settings'}
|
||||||
|
<Settings />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -154,7 +162,7 @@
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 15px;
|
font-size: 1.071rem;
|
||||||
}
|
}
|
||||||
.mark {
|
.mark {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -164,7 +172,7 @@
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--accent-text);
|
color: var(--accent-text);
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.links {
|
.links {
|
||||||
|
|
@ -184,7 +192,7 @@
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13.5px;
|
font-size: .964rem;
|
||||||
}
|
}
|
||||||
.navlink:hover {
|
.navlink:hover {
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
|
|
@ -196,13 +204,13 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.count {
|
.count {
|
||||||
font-size: 12px;
|
font-size: .857rem;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.section {
|
.section {
|
||||||
margin: 0 var(--space-2) -8px;
|
margin: 0 var(--space-2) -8px;
|
||||||
font-size: 11px;
|
font-size: .786rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.07em;
|
letter-spacing: 0.07em;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
|
|
@ -228,7 +236,7 @@
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--surface-3);
|
background: var(--surface-3);
|
||||||
font-size: 12px;
|
font-size: .857rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
@ -239,13 +247,13 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.who strong {
|
.who strong {
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.who .faint {
|
.who .faint {
|
||||||
font-size: 11px;
|
font-size: .786rem;
|
||||||
}
|
}
|
||||||
.main {
|
.main {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -274,7 +282,7 @@
|
||||||
height: 34px;
|
height: 34px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 1.143rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.back:hover {
|
.back:hover {
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.id .muted {
|
.id .muted {
|
||||||
font-size: 12px;
|
font-size: .857rem;
|
||||||
}
|
}
|
||||||
.grants {
|
.grants {
|
||||||
padding: 0 var(--space-4) var(--space-4);
|
padding: 0 var(--space-4) var(--space-4);
|
||||||
|
|
@ -214,7 +214,7 @@
|
||||||
}
|
}
|
||||||
.note {
|
.note {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: .857rem;
|
||||||
}
|
}
|
||||||
.grantrow {
|
.grantrow {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -223,10 +223,10 @@
|
||||||
}
|
}
|
||||||
.libname {
|
.libname {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
}
|
}
|
||||||
.libname .faint {
|
.libname .faint {
|
||||||
font-size: 11px;
|
font-size: .786rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +240,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-size: 13px;
|
font-size: .929rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export type View =
|
||||||
| { name: 'search'; query: string }
|
| { name: 'search'; query: string }
|
||||||
| { name: 'libraries' }
|
| { name: 'libraries' }
|
||||||
| { name: 'users' }
|
| { name: 'users' }
|
||||||
|
| { name: 'settings' }
|
||||||
|
|
||||||
export const view = writable<View>({ name: 'home' })
|
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