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. 138
      src/lib/components/content/RichTextEditor.svelte
  5. 24
      src/lib/components/write/AdvancedEditor.svelte
  6. 24
      src/lib/services/version-manager.ts

4
public/healthz.json

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

15
scripts/generate-healthz.js

@ -2,13 +2,24 @@
* Generate health check JSON file at build time * Generate health check JSON file at build time
*/ */
import { writeFileSync } from 'fs'; import { writeFileSync, readFileSync } from 'fs';
import { join } from 'path'; 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 = { const healthz = {
status: 'ok', status: 'ok',
service: 'aitherboard', service: 'aitherboard',
version: process.env.npm_package_version || '0.1.0', version: version,
buildTime: new Date().toISOString(), buildTime: new Date().toISOString(),
gitCommit: process.env.GIT_COMMIT || 'unknown', gitCommit: process.env.GIT_COMMIT || 'unknown',
timestamp: Date.now() timestamp: Date.now()

47
src/app.css

@ -1500,10 +1500,53 @@ body::before {
/* Fog theme dark mode - explicit override to ensure it works */ /* Fog theme dark mode - explicit override to ensure it works */
.dark [data-design-theme="fog"] .bg-fog-post, .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"] .bg-fog-surface,
.dark [data-design-theme="fog"] [class*="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;
} }

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

@ -146,17 +146,65 @@
</script> </script>
<div class="textarea-wrapper"> <div class="textarea-wrapper">
<div class="textarea-container">
<textarea <textarea
bind:this={textareaRef} bind:this={textareaRef}
bind:value bind:value
{placeholder} {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' : ''}" 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} {rows}
{disabled} {disabled}
oninput={(e) => { oninput={(e) => {
if (onValueChange) { if (onValueChange) {
onValueChange(e.currentTarget.value); 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) => { onkeydown={(e) => {
// CTRL+ENTER or CMD+ENTER to publish // CTRL+ENTER or CMD+ENTER to publish
@ -164,12 +212,73 @@
e.preventDefault(); e.preventDefault();
onPublish(); 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} {#if textareaRef}
<MentionsAutocomplete textarea={textareaRef} /> <MentionsAutocomplete textarea={textareaRef} />
{/if} {/if}
</div>
{#if showToolbar} {#if showToolbar}
<div class="textarea-buttons"> <div class="textarea-buttons">
@ -226,29 +335,34 @@
position: relative; position: relative;
} }
.textarea-container {
position: relative;
}
textarea { textarea {
resize: vertical; resize: vertical;
min-height: 100px; 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 { textarea:focus {
outline: none; outline: none;
border-color: var(--fog-accent, #64748b); border-color: var(--fog-accent, #64748b);
} }
.textarea-buttons { .textarea-buttons {
position: absolute;
bottom: 0.5rem;
left: 0.75rem;
display: flex; display: flex;
gap: 0.25rem; gap: 0.5rem;
z-index: 10; margin-top: 0.75rem;
padding-top: 0.5rem; /* Add padding above buttons to separate from text */ 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 { .upload-button {

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

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

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

@ -22,19 +22,35 @@ export async function getAppVersion(): Promise<string> {
try { try {
// Try to read from healthz.json (generated at build time) // 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) { if (response.ok) {
const data: HealthzData = await response.json(); const data: HealthzData = await response.json();
cachedVersion = (data.version || '0.2.0') as string; // Only use the version if it's not "unknown"
if (data.version && data.version !== 'unknown') {
cachedVersion = data.version as string;
cachedBuildTimestamp = data.timestamp || null; cachedBuildTimestamp = data.timestamp || null;
return cachedVersion; return cachedVersion;
} }
}
} catch (error) { } catch (error) {
// Failed to fetch healthz.json, use fallback // Failed to fetch healthz.json, use fallback
console.warn('Failed to fetch healthz.json:', error);
} }
// Fallback version (should match package.json) // 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; return cachedVersion;
} }
@ -62,7 +78,7 @@ export async function getBuildTimestamp(): Promise<number | null> {
* Get the current app version synchronously (uses cached value or fallback) * Get the current app version synchronously (uses cached value or fallback)
*/ */
export function getAppVersionSync(): string { export function getAppVersionSync(): string {
return cachedVersion || '0.2.0'; return cachedVersion || '0.2.1';
} }
/** /**

Loading…
Cancel
Save