diff --git a/app/assets/stylesheets/components/settings.css b/app/assets/stylesheets/components/settings.css index 100cbdf..90e2d1f 100644 --- a/app/assets/stylesheets/components/settings.css +++ b/app/assets/stylesheets/components/settings.css @@ -124,7 +124,8 @@ border-color: var(--color-border); } -.theme-choice { +.theme-choice, +.density-choice { display: flex; align-items: center; justify-content: space-between; @@ -135,18 +136,21 @@ border: var(--border-width) solid var(--color-border); } -.theme-choice-label { +.theme-choice-label, +.density-choice-label { font-weight: var(--font-weight-medium); color: var(--color-text-primary); } -.theme-choice-buttons { +.theme-choice-buttons, +.density-choice-buttons { display: inline-flex; align-items: center; gap: var(--spacing-2); } -.theme-choice-button { +.theme-choice-button, +.density-choice-button { display: inline-flex; align-items: center; gap: var(--spacing-2); @@ -159,19 +163,22 @@ transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast); } -.theme-choice-button:hover { +.theme-choice-button:hover, +.density-choice-button:hover { background: var(--color-bg-hover); box-shadow: var(--shadow-sm); transform: translateY(-1px); } -.theme-choice-button.is-active { +.theme-choice-button.is-active, +.density-choice-button.is-active { background: var(--color-bg-button); color: var(--color-text-button); border-color: transparent; } -.theme-choice-button.is-active i { +.theme-choice-button.is-active i, +.density-choice-button.is-active i { color: currentColor; } diff --git a/app/assets/stylesheets/variables.css b/app/assets/stylesheets/variables.css index 1079de0..d9e150d 100644 --- a/app/assets/stylesheets/variables.css +++ b/app/assets/stylesheets/variables.css @@ -60,3 +60,69 @@ --avatar-size-md: 32px; --avatar-size-default: 40px; } + +:root[data-density="tiny"] { + --font-size-xs: 0.625rem; + --font-size-sm: 0.675rem; + --font-size-base: 0.725rem; + --font-size-md: 0.775rem; + --font-size-lg: 0.825rem; + + --spacing-2: 0.25rem; + --spacing-3: 0.375rem; + --spacing-4: 0.625rem; + --spacing-5: 0.75rem; + --spacing-6: 1rem; + + --line-height-normal: 1.4; + --line-height-relaxed: 1.45; +} + +:root[data-density="compact"] { + --font-size-xs: 0.70rem; + --font-size-sm: 0.75rem; + --font-size-base: 0.80rem; + --font-size-md: 0.875rem; + --font-size-lg: 0.90rem; + + --spacing-2: 0.375rem; + --spacing-3: 0.5rem; + --spacing-4: 0.75rem; + --spacing-5: 1rem; + --spacing-6: 1.25rem; + + --line-height-normal: 1.5; + --line-height-relaxed: 1.55; +} + +:root[data-density="comfortable"] { + --font-size-xs: 0.875rem; + --font-size-sm: 0.95rem; + --font-size-base: 1rem; + --font-size-md: 1.1rem; + --font-size-lg: 1.15rem; + + --spacing-3: 0.875rem; + --spacing-4: 1.25rem; + --spacing-5: 1.5rem; + --spacing-6: 1.75rem; + + --line-height-normal: 1.65; + --line-height-relaxed: 1.80; +} + +:root[data-density="spacious"] { + --font-size-xs: 0.95rem; + --font-size-sm: 1.05rem; + --font-size-base: 1.1rem; + --font-size-md: 1.2rem; + --font-size-lg: 1.25rem; + + --spacing-3: 1rem; + --spacing-4: 1.5rem; + --spacing-5: 1.75rem; + --spacing-6: 2rem; + + --line-height-normal: 1.70; + --line-height-relaxed: 1.85; +} diff --git a/app/javascript/controllers/density_controller.js b/app/javascript/controllers/density_controller.js new file mode 100644 index 0000000..bdc4b42 --- /dev/null +++ b/app/javascript/controllers/density_controller.js @@ -0,0 +1,48 @@ +import { Controller } from "@hotwired/stimulus" + +const STORAGE_KEY = "hackorum-density" +const DEFAULT_DENSITY = "default" +const VALID_DENSITIES = ["tiny", "compact", "default", "comfortable", "spacious"] + +export default class extends Controller { + static targets = ["button"] + + connect() { + this.applyInitialDensity() + } + + select(event) { + event.preventDefault() + const { densityValue } = event.currentTarget.dataset + this.setDensity(densityValue) + } + + applyInitialDensity() { + const stored = window.localStorage.getItem(STORAGE_KEY) + const initial = VALID_DENSITIES.includes(stored) ? stored : DEFAULT_DENSITY + this.setDensity(initial, { persist: false }) + } + + setDensity(density, { persist = true } = {}) { + const normalized = VALID_DENSITIES.includes(density) ? density : DEFAULT_DENSITY + document.documentElement.dataset.density = normalized + + if (persist) { + window.localStorage.setItem(STORAGE_KEY, normalized) + } + + this.updateButtons(normalized) + } + + updateButtons(density) { + if (this.hasButtonTarget) { + this.buttonTargets.forEach((button) => { + button.classList.toggle("is-active", button.dataset.densityValue === density) + }) + } + } + + get currentDensity() { + return document.documentElement.dataset.density || DEFAULT_DENSITY + } +} diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 4103b92..e0ca9fa 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -1,5 +1,5 @@ doctype html -html data-theme="light" +html data-theme="light" data-density="default" head title = meta_title meta[name="description" content=meta_description] @@ -30,6 +30,10 @@ html data-theme="light" var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; var theme = stored || (prefersDark ? 'dark' : 'light'); document.documentElement.dataset.theme = theme; + var densityKey = 'hackorum-density'; + var validDensities = ['tiny', 'compact', 'default', 'comfortable', 'spacious']; + var storedDensity = window.localStorage.getItem(densityKey); + document.documentElement.dataset.density = validDensities.includes(storedDensity) ? storedDensity : 'default'; })(); = stylesheet_link_tag "application", "data-turbo-track": "reload" = javascript_importmap_tags @@ -37,7 +41,7 @@ html data-theme="light" - if ENV["UMAMI_WEBSITE_ID"].present? - umami_host = ENV["UMAMI_HOST"].presence || "https://umami.hackorum.dev" script[async defer data-website-id=ENV["UMAMI_WEBSITE_ID"] src="#{umami_host}/script.js"] - body class=(content_for?(:sidebar) ? "has-sidebar" : nil) data-controller="sidebar swipe-nav" + body class=(content_for?(:sidebar) ? "has-sidebar" : nil) data-controller="sidebar swipe-nav density" - if user_signed_in? && current_user.username.blank? .global-warning span Please set a username in Settings. diff --git a/app/views/settings/profiles/show.html.slim b/app/views/settings/profiles/show.html.slim index d8a2249..1dc9767 100644 --- a/app/views/settings/profiles/show.html.slim +++ b/app/views/settings/profiles/show.html.slim @@ -24,6 +24,23 @@ i.fa-regular.fa-moon aria-hidden="true" span Dark + .settings-section + h2 Text Density + p.settings-hint Choose your preferred text size and spacing. + .density-choice data-controller="density" + span.density-choice-label Density + .density-choice-buttons + button.density-choice-button type="button" data-density-value="tiny" data-density-target="button" data-action="click->density#select" + span Tiny + button.density-choice-button type="button" data-density-value="compact" data-density-target="button" data-action="click->density#select" + span Compact + button.density-choice-button type="button" data-density-value="default" data-density-target="button" data-action="click->density#select" + span Default + button.density-choice-button type="button" data-density-value="comfortable" data-density-target="button" data-action="click->density#select" + span Comfortable + button.density-choice-button type="button" data-density-value="spacious" data-density-target="button" data-action="click->density#select" + span Spacious + .settings-section h2 Mention Settings p.settings-hint Control who can @mention you in notes.