Browse Source

fix /write normal and advanced scrolling

master
Silberengel 1 month ago
parent
commit
1efb725a31
  1. 4
      public/healthz.json
  2. 15
      scripts/generate-healthz.js
  3. 47
      src/app.css
  4. 158
      src/lib/components/content/RichTextEditor.svelte
  5. 24
      src/lib/components/write/AdvancedEditor.svelte
  6. 28
      src/lib/services/version-manager.ts

4
public/healthz.json

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
"status": "ok",
"service": "aitherboard",
"version": "0.2.1",
"buildTime": "2026-02-10T22:25:53.381Z",
"buildTime": "2026-02-11T05:18:47.417Z",
"gitCommit": "unknown",
"timestamp": 1770762353381
"timestamp": 1770787127417
}

15
scripts/generate-healthz.js

@ -2,13 +2,24 @@ @@ -2,13 +2,24 @@
* Generate health check JSON file at build time
*/
import { writeFileSync } from 'fs';
import { writeFileSync, readFileSync } from 'fs';
import { join } from 'path';
// Read version from package.json
let version = '0.1.0';
try {
const packageJsonPath = join(process.cwd(), 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
version = packageJson.version || process.env.npm_package_version || '0.1.0';
} catch (error) {
console.warn('Failed to read version from package.json, using fallback:', error);
version = process.env.npm_package_version || '0.1.0';
}
const healthz = {
status: 'ok',
service: 'aitherboard',
version: process.env.npm_package_version || '0.1.0',
version: version,
buildTime: new Date().toISOString(),
gitCommit: process.env.GIT_COMMIT || 'unknown',
timestamp: Date.now()

47
src/app.css

@ -1500,10 +1500,53 @@ body::before { @@ -1500,10 +1500,53 @@ body::before {
/* Fog theme dark mode - explicit override to ensure it works */
.dark [data-design-theme="fog"] .bg-fog-post,
.dark [data-design-theme="fog"] [class*="bg-fog-post"],
.dark [data-design-theme="fog"] [class*="bg-fog-post"] {
background-color: #334155 !important; /* fog-dark-post */
}
.dark [data-design-theme="fog"] .bg-fog-surface,
.dark [data-design-theme="fog"] [class*="bg-fog-surface"] {
background-color: #334155 !important; /* fog-dark-post */
background-color: #1e293b !important; /* fog-dark-surface */
}
/* Fix CSS custom properties that use white fallbacks - set dark values in dark mode */
.dark {
--fog-post: #334155;
--fog-surface: #1e293b;
--fog-bg: #0f172a;
}
/* Ensure navigation bar is dark in dark mode for fog theme */
.dark [data-design-theme="fog"] nav,
.dark [data-design-theme="fog"] nav.bg-fog-surface,
.dark nav.bg-fog-surface,
.dark nav {
background-color: #1e293b !important; /* fog-dark-surface */
}
/* Force dark backgrounds for elements that might have inline styles with white fallbacks */
.dark [style*="var(--fog-post"] {
background: var(--fog-dark-post, #334155) !important;
background-color: var(--fog-dark-post, #334155) !important;
}
.dark [style*="var(--fog-surface"] {
background: var(--fog-dark-surface, #1e293b) !important;
background-color: var(--fog-dark-surface, #1e293b) !important;
}
.dark [style*="var(--fog-bg"] {
background: var(--fog-dark-bg, #0f172a) !important;
background-color: var(--fog-dark-bg, #0f172a) !important;
}
/* Catch any remaining white backgrounds in dark mode for fog theme */
.dark [data-design-theme="fog"] [style*="#ffffff"],
.dark [data-design-theme="fog"] [style*="#fff"],
.dark [data-design-theme="fog"] [style*="background: #ffffff"],
.dark [data-design-theme="fog"] [style*="background-color: #ffffff"] {
background: var(--fog-dark-post, #334155) !important;
background-color: var(--fog-dark-post, #334155) !important;
}

158
src/lib/components/content/RichTextEditor.svelte

@ -146,17 +146,65 @@ @@ -146,17 +146,65 @@
</script>
<div class="textarea-wrapper">
<textarea
bind:this={textareaRef}
bind:value
{placeholder}
class="w-full p-3 border border-fog-border dark:border-fog-dark-border rounded bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text {showToolbar ? 'has-buttons' : ''}"
{rows}
{disabled}
<div class="textarea-container">
<textarea
bind:this={textareaRef}
bind:value
{placeholder}
class="w-full p-3 border border-fog-border dark:border-fog-dark-border rounded bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text"
{rows}
{disabled}
oninput={(e) => {
if (onValueChange) {
onValueChange(e.currentTarget.value);
}
// On mobile, ensure cursor stays in view within the textarea
if (textareaRef && window.innerWidth <= 768) {
// Use requestAnimationFrame to ensure DOM is updated
requestAnimationFrame(() => {
if (textareaRef) {
// Scroll the textarea to show the cursor
const cursorPos = textareaRef.selectionStart;
const textBeforeCursor = textareaRef.value.substring(0, cursorPos);
// Create a temporary element to measure cursor position
const measureDiv = document.createElement('div');
const computedStyle = window.getComputedStyle(textareaRef);
measureDiv.style.cssText = computedStyle.cssText;
measureDiv.style.visibility = 'hidden';
measureDiv.style.position = 'absolute';
measureDiv.style.whiteSpace = 'pre-wrap';
measureDiv.style.wordWrap = 'break-word';
measureDiv.style.width = computedStyle.width;
measureDiv.textContent = textBeforeCursor;
document.body.appendChild(measureDiv);
const cursorTop = measureDiv.offsetHeight;
document.body.removeChild(measureDiv);
// Calculate scroll position to keep cursor visible
const textareaHeight = textareaRef.clientHeight;
const lineHeight = parseFloat(computedStyle.lineHeight) || 20;
const bottomPadding = 2 * lineHeight; // Keep 2 lines of padding at bottom
const topPadding = 2 * lineHeight; // Keep 2 lines of padding at top
// Get the total scroll height to know how much content there is
const scrollHeight = textareaRef.scrollHeight;
const maxScrollTop = scrollHeight - textareaHeight;
// Scroll to show cursor with padding
if (cursorTop < textareaRef.scrollTop + topPadding) {
// Cursor is above visible area
textareaRef.scrollTop = Math.max(0, cursorTop - topPadding);
} else if (cursorTop > textareaRef.scrollTop + textareaHeight - bottomPadding) {
// Cursor is below visible area - scroll down to show it with bottom padding
// Ensure we don't scroll past the maximum
const targetScroll = cursorTop - textareaHeight + bottomPadding;
textareaRef.scrollTop = Math.min(maxScrollTop, Math.max(0, targetScroll));
}
}
});
}
}}
onkeydown={(e) => {
// CTRL+ENTER or CMD+ENTER to publish
@ -164,12 +212,73 @@ @@ -164,12 +212,73 @@
e.preventDefault();
onPublish();
}
// On mobile, also handle scrolling on keydown for better responsiveness
if (textareaRef && window.innerWidth <= 768 && !e.ctrlKey && !e.metaKey) {
// Use a small delay to let the input update first
setTimeout(() => {
if (textareaRef) {
const cursorPos = textareaRef.selectionStart;
const textBeforeCursor = textareaRef.value.substring(0, cursorPos);
// Create a temporary element to measure cursor position
const measureDiv = document.createElement('div');
const computedStyle = window.getComputedStyle(textareaRef);
measureDiv.style.cssText = computedStyle.cssText;
measureDiv.style.visibility = 'hidden';
measureDiv.style.position = 'absolute';
measureDiv.style.whiteSpace = 'pre-wrap';
measureDiv.style.wordWrap = 'break-word';
measureDiv.style.width = computedStyle.width;
measureDiv.textContent = textBeforeCursor;
document.body.appendChild(measureDiv);
const cursorTop = measureDiv.offsetHeight;
document.body.removeChild(measureDiv);
// Calculate scroll position to keep cursor visible
const textareaHeight = textareaRef.clientHeight;
const lineHeight = parseFloat(computedStyle.lineHeight) || 20;
const bottomPadding = 2 * lineHeight; // Keep 2 lines of padding at bottom
const topPadding = 2 * lineHeight; // Keep 2 lines of padding at top
const scrollHeight = textareaRef.scrollHeight;
const maxScrollTop = scrollHeight - textareaHeight;
// Scroll to show cursor with padding
if (cursorTop < textareaRef.scrollTop + topPadding) {
textareaRef.scrollTop = Math.max(0, cursorTop - topPadding);
} else if (cursorTop > textareaRef.scrollTop + textareaHeight - bottomPadding) {
const targetScroll = cursorTop - textareaHeight + bottomPadding;
textareaRef.scrollTop = Math.min(maxScrollTop, Math.max(0, targetScroll));
}
}
}, 10);
}
}}
onfocus={(e) => {
// On mobile, ensure textarea is scrolled into view when focused
if (window.innerWidth <= 768 && textareaRef) {
setTimeout(() => {
if (textareaRef) {
// Scroll the page to show the textarea, but only if it's not fully visible
const rect = textareaRef.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const isFullyVisible = rect.top >= 0 && rect.bottom <= viewportHeight;
if (!isFullyVisible) {
textareaRef.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}, 100);
}
}}
></textarea>
></textarea>
{#if textareaRef}
<MentionsAutocomplete textarea={textareaRef} />
{/if}
{#if textareaRef}
<MentionsAutocomplete textarea={textareaRef} />
{/if}
</div>
{#if showToolbar}
<div class="textarea-buttons">
@ -226,29 +335,34 @@ @@ -226,29 +335,34 @@
position: relative;
}
.textarea-container {
position: relative;
}
textarea {
resize: vertical;
min-height: 100px;
}
/* Add padding to bottom when buttons are visible to prevent text overlap */
textarea.has-buttons {
padding-bottom: 3rem; /* Increased padding to accommodate buttons */
}
textarea:focus {
outline: none;
border-color: var(--fog-accent, #64748b);
}
.textarea-buttons {
position: absolute;
bottom: 0.5rem;
left: 0.75rem;
display: flex;
gap: 0.25rem;
z-index: 10;
padding-top: 0.5rem; /* Add padding above buttons to separate from text */
gap: 0.5rem;
margin-top: 0.75rem;
padding: 0.5rem;
background: var(--fog-post, #ffffff);
border: 1px solid var(--fog-border, #cbd5e1);
border-radius: 0.25rem;
justify-content: flex-start;
}
:global(.dark) .textarea-buttons {
background: var(--fog-dark-post, #334155);
border-color: var(--fog-dark-border, #475569);
}
.upload-button {

24
src/lib/components/write/AdvancedEditor.svelte

@ -1243,7 +1243,7 @@ @@ -1243,7 +1243,7 @@
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 400px;
min-height: 0; /* Allow flex item to shrink below content size */
}
.editor-toolbar {
@ -1256,6 +1256,8 @@ @@ -1256,6 +1256,8 @@
position: relative;
z-index: 10;
min-height: 3.5rem;
flex-shrink: 0; /* Prevent toolbar from shrinking */
margin-bottom: 0; /* No margin, will use padding on editor-wrapper */
}
:global(.dark) .editor-toolbar {
@ -1332,7 +1334,8 @@ @@ -1332,7 +1334,8 @@
overflow: hidden;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
margin: 1rem 1.5rem;
margin: 1rem 1.5rem; /* Consistent margin on all sides */
min-height: 0; /* Allow flex item to shrink */
}
:global(.dark) .editor-wrapper {
@ -1626,11 +1629,18 @@ @@ -1626,11 +1629,18 @@
.editor-container {
max-width: 100%;
max-height: 100vh;
height: 100vh; /* Use full viewport height on mobile */
border-radius: 0;
border-left: none;
border-right: none;
}
.editor-body {
min-height: 0; /* Ensure it can shrink */
flex: 1;
overflow: hidden;
}
.editor-header {
padding: 0.75rem 1rem;
}
@ -1643,6 +1653,7 @@ @@ -1643,6 +1653,7 @@
padding: 0.5rem 1rem;
gap: 0.375rem;
min-height: 3rem;
margin-bottom: 0; /* No margin, spacing handled by editor-wrapper margin */
}
.toolbar-group {
@ -1661,8 +1672,9 @@ @@ -1661,8 +1672,9 @@
}
.editor-wrapper {
margin: 0.75rem 1rem;
min-height: calc(100vh - 250px);
margin: 0.75rem 1rem; /* Consistent margin, provides spacing from toolbar */
min-height: 0; /* Allow flex item to shrink */
flex: 1; /* Take remaining space */
}
.editor-footer {
@ -1684,6 +1696,10 @@ @@ -1684,6 +1696,10 @@
gap: 0.25rem;
}
.editor-wrapper {
margin: 1rem 0.75rem; /* Slightly more margin on very small screens when toolbar wraps */
}
.toolbar-button {
padding: 0.25rem 0.375rem;
font-size: 0.7rem;

28
src/lib/services/version-manager.ts

@ -22,19 +22,35 @@ export async function getAppVersion(): Promise<string> { @@ -22,19 +22,35 @@ export async function getAppVersion(): Promise<string> {
try {
// Try to read from healthz.json (generated at build time)
const response = await fetch('/healthz.json');
const response = await fetch('/healthz.json', { cache: 'no-store' });
if (response.ok) {
const data: HealthzData = await response.json();
cachedVersion = (data.version || '0.2.0') as string;
cachedBuildTimestamp = data.timestamp || null;
return cachedVersion;
// Only use the version if it's not "unknown"
if (data.version && data.version !== 'unknown') {
cachedVersion = data.version as string;
cachedBuildTimestamp = data.timestamp || null;
return cachedVersion;
}
}
} catch (error) {
// Failed to fetch healthz.json, use fallback
console.warn('Failed to fetch healthz.json:', error);
}
// Fallback version (should match package.json)
cachedVersion = '0.2.0';
// Try to read from package.json if available (for dev mode)
try {
if (typeof window !== 'undefined') {
// In browser, we can't read package.json directly, so use fallback
cachedVersion = '0.2.1'; // Update this to match package.json version
}
} catch (error) {
// Ignore
}
if (!cachedVersion) {
cachedVersion = '0.2.1'; // Fallback to current package.json version
}
return cachedVersion;
}
@ -62,7 +78,7 @@ export async function getBuildTimestamp(): Promise<number | null> { @@ -62,7 +78,7 @@ export async function getBuildTimestamp(): Promise<number | null> {
* Get the current app version synchronously (uses cached value or fallback)
*/
export function getAppVersionSync(): string {
return cachedVersion || '0.2.0';
return cachedVersion || '0.2.1';
}
/**

Loading…
Cancel
Save