mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-30 20:29:15 +00:00
feat: add Reduced motion setting and polish chrome, files, and stack-detail (#1501)
A batch of UI/UX polish: - New independent "Reduced motion" appearance setting (separate from Reduced effects). Drives framer-motion via MotionConfig and clamps CSS transitions via data-motion on <html>; toasts are unaffected. Defaults off (OS preference still honored). - Stack-detail Files tab: rename "Files & Volumes" to "Files", add a persisted word-wrap toggle to the file viewer (default on), and add a fullscreen toggle that collapses the Command Center + Logs column so the editor fills the width. - Create Stack > From Git: remove the nested scroll clamp so the deploy toggle and footer are reachable. - Fleet: full-width tab band with icon-only Refresh / Export Dossier, icon-only Check-for-updates / Add-node on the Overview toolbar, theme-aware empty-state headings (calm drops the italic), and fix the Actions card body overlapping the action-row divider. - Snapshots: restyle Restore and Restore all to the ghost button design used by View / Preview / Download, and right-align the per-stack Restore. - Settings sidebar: App Store gradient active style and standard font size. - Compose Doctor: dismiss the high-risk banner (and clear the tab dot) until the findings change, via a shared fingerprint-keyed hook. - Stack-detail Storage: link the "no recent fleet snapshot" warning to the Fleet Snapshots tab (FleetView tabs are now controlled to support the deep link).
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { useState, useEffect, useMemo, useRef, Suspense } from 'react';
|
||||
import { Editor } from '@/lib/monacoLoader';
|
||||
import { AlertCircle, FileIcon, Download, Loader2, Save } from 'lucide-react';
|
||||
import { AlertCircle, FileIcon, Download, Loader2, Save, WrapText } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
import { readStackFile, writeStackFile, downloadStackFile, FileConflictError } from '@/lib/stackFilesApi';
|
||||
import { extensionToLanguage } from '@/lib/monacoLanguages';
|
||||
import { formatBytes } from '@/lib/utils';
|
||||
import { cn, formatBytes } from '@/lib/utils';
|
||||
|
||||
const WORD_WRAP_KEY = 'sencho.fileViewer.wordWrap';
|
||||
|
||||
interface FileViewerProps {
|
||||
stackName: string;
|
||||
@@ -131,6 +133,15 @@ export function FileViewer({
|
||||
const readOnly = !canEdit;
|
||||
const hasChanges = content !== originalContent;
|
||||
|
||||
// Word wrap, persisted across files and sessions. Defaults on so wide files
|
||||
// do not require horizontal scrolling; only an explicit 'false' disables it.
|
||||
const [wordWrap, setWordWrap] = useState(() => {
|
||||
try { return localStorage.getItem(WORD_WRAP_KEY) !== 'false'; } catch { return true; }
|
||||
});
|
||||
useEffect(() => {
|
||||
try { localStorage.setItem(WORD_WRAP_KEY, String(wordWrap)); } catch { /* ignore */ }
|
||||
}, [wordWrap]);
|
||||
|
||||
// Stash the latest callback in a ref so the unmount-cleanup effect can be
|
||||
// truly unmount-scoped without re-running every time a parent passes a fresh
|
||||
// function identity.
|
||||
@@ -164,8 +175,9 @@ export function FileViewer({
|
||||
fontSize: 13,
|
||||
padding: { top: 8 },
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: wordWrap ? ('on' as const) : ('off' as const),
|
||||
}),
|
||||
[readOnly],
|
||||
[readOnly, wordWrap],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -350,6 +362,17 @@ export function FileViewer({
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-1.5 border-b border-glass-border shrink-0">
|
||||
<span className="font-mono text-xs text-stat-subtitle truncate">{filename}</span>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className={cn('h-7 w-7 p-0', wordWrap ? 'text-brand' : 'text-stat-subtitle')}
|
||||
onClick={() => setWordWrap((v) => !v)}
|
||||
aria-pressed={wordWrap}
|
||||
title={wordWrap ? 'Word wrap on' : 'Word wrap off'}
|
||||
aria-label={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
|
||||
>
|
||||
<WrapText className="w-3.5 h-3.5" strokeWidth={1.5} />
|
||||
</Button>
|
||||
{readOnly && (
|
||||
<span className="text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle border border-border rounded px-1.5 py-0.5">
|
||||
Read-only
|
||||
|
||||
Reference in New Issue
Block a user