optional indentation guides in the compose/YAML editor
This commit is contained in:
10 files changed
+109
-15
No files matched your search
@@ -73,6 +73,7 @@
|
||||
"@codemirror/view": "6.39.11",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lucide/lab": "0.1.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"ansi_up": "6.0.6",
|
||||
"argon2": "0.41.1",
|
||||
"better-sqlite3": "11.7.0",
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap, type CompletionContext, type CompletionResult } from '@codemirror/autocomplete';
|
||||
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
||||
import { indentationMarkers } from '@replit/codemirror-indentation-markers';
|
||||
import { themeStore } from '$lib/stores/theme';
|
||||
import { get } from 'svelte/store';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
@@ -707,6 +710,19 @@
|
||||
EditorView.lineWrapping,
|
||||
EditorState.tabSize.of(2),
|
||||
getLanguageExtension(language),
|
||||
// Vertical indentation guides, opt-in per user (#1410). Subtle tones for both themes.
|
||||
...(get(themeStore).editorIndentGuides
|
||||
? [indentationMarkers({
|
||||
highlightActiveBlock: true,
|
||||
hideFirstIndent: true,
|
||||
colors: {
|
||||
light: '#e2e8f0',
|
||||
dark: '#3a3f4b',
|
||||
activeLight: '#94a3b8',
|
||||
activeDark: '#5b6472'
|
||||
}
|
||||
})]
|
||||
: []),
|
||||
...(language === 'yaml' ? [Prec.high(keymap.of([{ key: 'Enter', run: yamlNewlineAndIndent }]))] : [])
|
||||
].flat();
|
||||
|
||||
@@ -836,16 +852,18 @@
|
||||
// Track previous values for comparison
|
||||
let prevLanguage = $state(language);
|
||||
let prevTheme = $state(theme);
|
||||
let prevIndentGuides = $state($themeStore.editorIndentGuides);
|
||||
|
||||
// Recreate editor if language or theme changes
|
||||
// Recreate editor if language, theme, or the indent-guides preference changes
|
||||
$effect(() => {
|
||||
const currentLanguage = language;
|
||||
const currentTheme = theme;
|
||||
const currentIndentGuides = $themeStore.editorIndentGuides;
|
||||
|
||||
// Only recreate if language or theme actually changed
|
||||
if (view && (currentLanguage !== prevLanguage || currentTheme !== prevTheme)) {
|
||||
if (view && (currentLanguage !== prevLanguage || currentTheme !== prevTheme || currentIndentGuides !== prevIndentGuides)) {
|
||||
prevLanguage = currentLanguage;
|
||||
prevTheme = currentTheme;
|
||||
prevIndentGuides = currentIndentGuides;
|
||||
const currentContent = view.state.doc.toString();
|
||||
destroyEditor();
|
||||
value = currentContent; // Preserve content
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { TogglePill } from '$lib/components/ui/toggle-pill';
|
||||
import { themeStore } from '$lib/stores/theme';
|
||||
import { authStore } from '$lib/stores/auth';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
interface Props {
|
||||
userId?: number; // omit for global default (login page / auth-disabled)
|
||||
}
|
||||
|
||||
let { userId }: Props = $props();
|
||||
|
||||
// Same "skip applying" rule as ThemeSelector: don't touch the live editor when the
|
||||
// admin is editing the global default while logged in (their own per-user preference
|
||||
// still drives their session).
|
||||
const skipApply = $derived($authStore.loading ? true : ($authStore.authEnabled && !userId));
|
||||
|
||||
let checked = $state(false);
|
||||
$effect(() => {
|
||||
checked = $themeStore.editorIndentGuides;
|
||||
});
|
||||
|
||||
function onToggle(value: boolean) {
|
||||
checked = value;
|
||||
themeStore.setPreference('editorIndentGuides', value, userId, skipApply);
|
||||
toast.success(value ? 'Indentation guides enabled' : 'Indentation guides disabled');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<Label>Indentation guides</Label>
|
||||
<TogglePill {checked} onchange={onToggle} />
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">Vertical guides showing nesting depth in the code editor.</p>
|
||||
</div>
|
||||
+11
-4
@@ -536,8 +536,9 @@ export async function getUserThemePreferences(userId: number): Promise<{
|
||||
animateIcons: boolean;
|
||||
coloredActionButtons: boolean;
|
||||
actionIconSize: string;
|
||||
editorIndentGuides: boolean;
|
||||
}> {
|
||||
const [lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, animateIcons, coloredActionButtons, actionIconSize] = await Promise.all([
|
||||
const [lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, animateIcons, coloredActionButtons, actionIconSize, editorIndentGuides] = await Promise.all([
|
||||
getUserSetting(userId, 'light_theme'),
|
||||
getUserSetting(userId, 'dark_theme'),
|
||||
getUserSetting(userId, 'font'),
|
||||
@@ -547,7 +548,8 @@ export async function getUserThemePreferences(userId: number): Promise<{
|
||||
getUserSetting(userId, 'editor_font'),
|
||||
getUserSetting(userId, 'animate_icons'),
|
||||
getUserSetting(userId, 'colored_action_buttons'),
|
||||
getUserSetting(userId, 'action_icon_size')
|
||||
getUserSetting(userId, 'action_icon_size'),
|
||||
getUserSetting(userId, 'editor_indent_guides')
|
||||
]);
|
||||
return {
|
||||
lightTheme: lightTheme || 'default',
|
||||
@@ -561,13 +563,15 @@ export async function getUserThemePreferences(userId: number): Promise<{
|
||||
animateIcons: animateIcons === 'false' ? false : true,
|
||||
// Default OFF — only true when explicitly stored
|
||||
coloredActionButtons: coloredActionButtons === 'true',
|
||||
actionIconSize: actionIconSize || 'normal'
|
||||
actionIconSize: actionIconSize || 'normal',
|
||||
// Default OFF — only true when explicitly stored (#1410)
|
||||
editorIndentGuides: editorIndentGuides === 'true'
|
||||
};
|
||||
}
|
||||
|
||||
export async function setUserThemePreferences(
|
||||
userId: number,
|
||||
prefs: { lightTheme?: string; darkTheme?: string; font?: string; fontSize?: string; gridFontSize?: string; terminalFont?: string; editorFont?: string; animateIcons?: boolean; coloredActionButtons?: boolean; actionIconSize?: string }
|
||||
prefs: { lightTheme?: string; darkTheme?: string; font?: string; fontSize?: string; gridFontSize?: string; terminalFont?: string; editorFont?: string; animateIcons?: boolean; coloredActionButtons?: boolean; actionIconSize?: string; editorIndentGuides?: boolean }
|
||||
): Promise<void> {
|
||||
const updates: Promise<void>[] = [];
|
||||
if (prefs.lightTheme !== undefined) {
|
||||
@@ -600,6 +604,9 @@ export async function setUserThemePreferences(
|
||||
if (prefs.actionIconSize !== undefined) {
|
||||
updates.push(setUserSetting(userId, 'action_icon_size', prefs.actionIconSize));
|
||||
}
|
||||
if (prefs.editorIndentGuides !== undefined) {
|
||||
updates.push(setUserSetting(userId, 'editor_indent_guides', prefs.editorIndentGuides ? 'true' : 'false'));
|
||||
}
|
||||
await Promise.all(updates);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface ThemePreferences {
|
||||
animateIcons: boolean;
|
||||
coloredActionButtons: boolean;
|
||||
actionIconSize: ActionIconSize;
|
||||
editorIndentGuides: boolean;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'dockhand-theme';
|
||||
@@ -48,7 +49,8 @@ const defaultPrefs: ThemePreferences = {
|
||||
editorFont: 'system-mono',
|
||||
animateIcons: true,
|
||||
coloredActionButtons: false,
|
||||
actionIconSize: 'normal'
|
||||
actionIconSize: 'normal',
|
||||
editorIndentGuides: false
|
||||
};
|
||||
|
||||
// Font size scale mapping
|
||||
@@ -125,7 +127,9 @@ function createThemeStore() {
|
||||
: !!(data.animateIcons ?? data.animate_icons),
|
||||
// Default OFF (#1072)
|
||||
coloredActionButtons: !!(data.coloredActionButtons ?? data.colored_action_buttons ?? false),
|
||||
actionIconSize: (data.actionIconSize || data.action_icon_size || 'normal') as ActionIconSize
|
||||
actionIconSize: (data.actionIconSize || data.action_icon_size || 'normal') as ActionIconSize,
|
||||
// Default OFF (#1410)
|
||||
editorIndentGuides: !!(data.editorIndentGuides ?? data.editor_indent_guides ?? false)
|
||||
};
|
||||
set(prefs);
|
||||
saveToStorage(prefs);
|
||||
|
||||
@@ -47,7 +47,7 @@ export const PUT: RequestHandler = async ({ request, cookies }) => {
|
||||
|
||||
const validActionIconSizes = ['small', 'normal', 'large', 'xlarge'];
|
||||
|
||||
const updates: { lightTheme?: string; darkTheme?: string; font?: string; fontSize?: string; gridFontSize?: string; terminalFont?: string; editorFont?: string; animateIcons?: boolean; coloredActionButtons?: boolean; actionIconSize?: string } = {};
|
||||
const updates: { lightTheme?: string; darkTheme?: string; font?: string; fontSize?: string; gridFontSize?: string; terminalFont?: string; editorFont?: string; animateIcons?: boolean; coloredActionButtons?: boolean; actionIconSize?: string; editorIndentGuides?: boolean } = {};
|
||||
|
||||
if (data.lightTheme !== undefined) {
|
||||
if (!validLightThemeIds.includes(data.lightTheme)) {
|
||||
@@ -105,6 +105,13 @@ export const PUT: RequestHandler = async ({ request, cookies }) => {
|
||||
updates.animateIcons = data.animateIcons;
|
||||
}
|
||||
|
||||
if (data.editorIndentGuides !== undefined) {
|
||||
if (typeof data.editorIndentGuides !== 'boolean') {
|
||||
return json({ error: 'Invalid editorIndentGuides' }, { status: 400 });
|
||||
}
|
||||
updates.editorIndentGuides = data.editorIndentGuides;
|
||||
}
|
||||
|
||||
if (data.coloredActionButtons !== undefined) {
|
||||
if (typeof data.coloredActionButtons !== 'boolean') {
|
||||
return json({ error: 'Invalid coloredActionButtons' }, { status: 400 });
|
||||
|
||||
@@ -117,6 +117,8 @@ export interface GeneralSettings {
|
||||
showWhatsNew: boolean;
|
||||
// Whether spinning icons (animate-spin etc.) are animated (#1169)
|
||||
animateIcons: boolean;
|
||||
// Vertical indentation guides in the code editor (#1410)
|
||||
editorIndentGuides: boolean;
|
||||
// Skip Dockhand's scanner images (grype, trivy) during 'prune all unused' (#625)
|
||||
protectScannerImages: boolean;
|
||||
// Scanner Advanced settings (#1219). Empty = use auto-detection.
|
||||
@@ -161,6 +163,7 @@ const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRe
|
||||
showImageChangelogLinks: true,
|
||||
showWhatsNew: true,
|
||||
animateIcons: true,
|
||||
editorIndentGuides: false,
|
||||
protectScannerImages: true,
|
||||
defaultScannerNetworkMode: '',
|
||||
defaultScannerDns: [],
|
||||
@@ -269,6 +272,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
showImageChangelogLinks,
|
||||
showWhatsNew,
|
||||
animateIcons,
|
||||
editorIndentGuides,
|
||||
protectScannerImages,
|
||||
defaultScannerNetworkMode,
|
||||
defaultScannerDnsRaw
|
||||
@@ -319,6 +323,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
getSetting('show_image_changelog_links'),
|
||||
getSetting('show_whats_new'),
|
||||
getSetting('animate_icons'),
|
||||
getSetting('editor_indent_guides'),
|
||||
getSetting('protect_scanner_images'),
|
||||
getSetting('default_scanner_network_mode'),
|
||||
getSetting('default_scanner_dns')
|
||||
@@ -373,6 +378,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
showImageChangelogLinks: showImageChangelogLinks ?? DEFAULT_SETTINGS.showImageChangelogLinks,
|
||||
showWhatsNew: showWhatsNew ?? DEFAULT_SETTINGS.showWhatsNew,
|
||||
animateIcons: animateIcons ?? DEFAULT_SETTINGS.animateIcons,
|
||||
editorIndentGuides: editorIndentGuides ?? DEFAULT_SETTINGS.editorIndentGuides,
|
||||
protectScannerImages: protectScannerImages ?? DEFAULT_SETTINGS.protectScannerImages,
|
||||
defaultScannerNetworkMode: defaultScannerNetworkMode ?? DEFAULT_SETTINGS.defaultScannerNetworkMode,
|
||||
defaultScannerDns: parseScannerDnsStorage(defaultScannerDnsRaw)
|
||||
@@ -393,7 +399,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { confirmDestructive, showStoppedContainers, highlightUpdates, coloredActionButtons, actionIconSize, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, logBufferSizeKb, logMaxLines, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, showExposedPorts, showGitCommitHash, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode, defaultBackupImage, honorProxyLabels, showImageChangelogLinks, animateIcons, protectScannerImages, showWhatsNew, defaultScannerNetworkMode, defaultScannerDns } = body;
|
||||
const { confirmDestructive, showStoppedContainers, highlightUpdates, coloredActionButtons, actionIconSize, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, logBufferSizeKb, logMaxLines, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, showExposedPorts, showGitCommitHash, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode, defaultBackupImage, honorProxyLabels, showImageChangelogLinks, animateIcons, editorIndentGuides, protectScannerImages, showWhatsNew, defaultScannerNetworkMode, defaultScannerDns } = body;
|
||||
|
||||
if (confirmDestructive !== undefined) {
|
||||
await setSetting('confirm_destructive', confirmDestructive);
|
||||
@@ -557,6 +563,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
if (animateIcons !== undefined && typeof animateIcons === 'boolean') {
|
||||
await setSetting('animate_icons', animateIcons);
|
||||
}
|
||||
if (editorIndentGuides !== undefined && typeof editorIndentGuides === 'boolean') {
|
||||
await setSetting('editor_indent_guides', editorIndentGuides);
|
||||
}
|
||||
if (protectScannerImages !== undefined && typeof protectScannerImages === 'boolean') {
|
||||
await setSetting('protect_scanner_images', protectScannerImages);
|
||||
}
|
||||
@@ -623,6 +632,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
showImageChangelogLinksVal,
|
||||
showWhatsNewVal,
|
||||
animateIconsVal,
|
||||
editorIndentGuidesVal,
|
||||
protectScannerImagesVal,
|
||||
defaultScannerNetworkModeVal,
|
||||
defaultScannerDnsRawVal
|
||||
@@ -673,6 +683,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
getSetting('show_image_changelog_links'),
|
||||
getSetting('show_whats_new'),
|
||||
getSetting('animate_icons'),
|
||||
getSetting('editor_indent_guides'),
|
||||
getSetting('protect_scanner_images'),
|
||||
getSetting('default_scanner_network_mode'),
|
||||
getSetting('default_scanner_dns')
|
||||
@@ -728,6 +739,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
showImageChangelogLinks: showImageChangelogLinksVal ?? DEFAULT_SETTINGS.showImageChangelogLinks,
|
||||
showWhatsNew: showWhatsNewVal ?? DEFAULT_SETTINGS.showWhatsNew,
|
||||
animateIcons: animateIconsVal ?? DEFAULT_SETTINGS.animateIcons,
|
||||
editorIndentGuides: editorIndentGuidesVal ?? DEFAULT_SETTINGS.editorIndentGuides,
|
||||
defaultScannerNetworkMode: defaultScannerNetworkModeVal ?? DEFAULT_SETTINGS.defaultScannerNetworkMode,
|
||||
defaultScannerDns: parseScannerDnsStorage(defaultScannerDnsRawVal)
|
||||
};
|
||||
|
||||
@@ -16,7 +16,8 @@ const DEFAULT_THEME_SETTINGS = {
|
||||
editorFont: 'system-mono',
|
||||
animateIcons: true,
|
||||
coloredActionButtons: false,
|
||||
actionIconSize: 'normal'
|
||||
actionIconSize: 'normal',
|
||||
editorIndentGuides: false
|
||||
};
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
@@ -31,7 +32,8 @@ export const GET: RequestHandler = async () => {
|
||||
editorFont,
|
||||
animateIcons,
|
||||
coloredActionButtons,
|
||||
actionIconSize
|
||||
actionIconSize,
|
||||
editorIndentGuides
|
||||
] = await Promise.all([
|
||||
getSetting('theme_light'),
|
||||
getSetting('theme_dark'),
|
||||
@@ -42,7 +44,8 @@ export const GET: RequestHandler = async () => {
|
||||
getSetting('theme_editor_font'),
|
||||
getSetting('animate_icons'),
|
||||
getSetting('colored_action_buttons'),
|
||||
getSetting('action_icon_size')
|
||||
getSetting('action_icon_size'),
|
||||
getSetting('editor_indent_guides')
|
||||
]);
|
||||
|
||||
return json({
|
||||
@@ -55,7 +58,8 @@ export const GET: RequestHandler = async () => {
|
||||
editorFont: editorFont ?? DEFAULT_THEME_SETTINGS.editorFont,
|
||||
animateIcons: animateIcons ?? DEFAULT_THEME_SETTINGS.animateIcons,
|
||||
coloredActionButtons: coloredActionButtons ?? DEFAULT_THEME_SETTINGS.coloredActionButtons,
|
||||
actionIconSize: actionIconSize ?? DEFAULT_THEME_SETTINGS.actionIconSize
|
||||
actionIconSize: actionIconSize ?? DEFAULT_THEME_SETTINGS.actionIconSize,
|
||||
editorIndentGuides: editorIndentGuides ?? DEFAULT_THEME_SETTINGS.editorIndentGuides
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get theme settings:', error);
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
import ThemeSelector from '$lib/components/ThemeSelector.svelte';
|
||||
import NavigationSelector from '$lib/components/NavigationSelector.svelte';
|
||||
import AnimateIconsToggle from '$lib/components/AnimateIconsToggle.svelte';
|
||||
import IndentGuidesToggle from '$lib/components/IndentGuidesToggle.svelte';
|
||||
import ColoredActionsToggle from '$lib/components/ColoredActionsToggle.svelte';
|
||||
import { themeStore } from '$lib/stores/theme';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
@@ -731,6 +732,7 @@
|
||||
<ThemeSelector userId={profile.id} />
|
||||
<ColoredActionsToggle userId={profile.id} />
|
||||
<AnimateIconsToggle userId={profile.id} />
|
||||
<IndentGuidesToggle userId={profile.id} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import ThemeSelector from '$lib/components/ThemeSelector.svelte';
|
||||
import NavigationSelector from '$lib/components/NavigationSelector.svelte';
|
||||
import AnimateIconsToggle from '$lib/components/AnimateIconsToggle.svelte';
|
||||
import IndentGuidesToggle from '$lib/components/IndentGuidesToggle.svelte';
|
||||
import ColoredActionsToggle from '$lib/components/ColoredActionsToggle.svelte';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
|
||||
@@ -511,6 +512,7 @@ services:
|
||||
<ThemeSelector />
|
||||
<ColoredActionsToggle />
|
||||
<AnimateIconsToggle />
|
||||
<IndentGuidesToggle />
|
||||
{#if $authStore.authEnabled}
|
||||
<div class="text-xs text-muted-foreground flex items-start gap-1.5 mt-2 p-2 bg-muted/50 rounded-md">
|
||||
<HelpCircle class="w-3.5 h-3.5 shrink-0 mt-0.5" />
|
||||
|
||||
Reference in new issue
Block a user