From aa6a9d299383670ec54193f62a3e161b07a1dfc5 Mon Sep 17 00:00:00 2001 From: Aaron Feledy Date: Fri, 7 Feb 2025 22:18:49 -0600 Subject: [PATCH 1/3] lint before deploy --- .github/workflows/deploy.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e43f57a..e43f477 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,6 +32,14 @@ jobs: - name: Install dependencies run: npm ci + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: latest + + - name: Run Biome + run: biome ci . + - name: Build run: npm run build @@ -52,4 +60,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 From 9feb45f4b15db6065dc8ded3a2c1cb15ce8dcdb6 Mon Sep 17 00:00:00 2001 From: Aaron Feledy Date: Fri, 7 Feb 2025 23:13:26 -0600 Subject: [PATCH 2/3] organize the libs --- src/components/ui/alert-dialog.jsx | 96 ++- src/components/ui/badge.jsx | 19 + src/components/ui/button.jsx | 22 + src/components/ui/dialog.jsx | 80 ++ src/components/ui/editor-menu.jsx | 150 +++- src/components/ui/editor.jsx | 436 +++++------ src/components/ui/input.jsx | 13 +- src/components/ui/share-dialog.jsx | 1 + src/components/ui/toast.tsx | 75 ++ src/components/ui/toaster.tsx | 6 +- src/hooks/use-toast.ts | 123 ++- src/{ => lib}/completions.js | 184 ++--- src/{ => lib}/constants.js | 0 src/{ => lib}/debug.js | 0 src/lib/dialog.js | 28 + src/lib/editor-config.js | 65 ++ src/lib/format-yaml.js | 28 +- src/lib/schema-validation.js | 1091 +++++++++++++++++++++++++++ src/{ => lib}/share.js | 10 +- src/lib/storage.js | 14 + src/lib/theme.js | 33 +- src/lib/utils.js | 6 - src/lib/utils.ts | 16 + src/schema.js | 839 -------------------- src/templates/example-landofile.yml | 16 + 25 files changed, 2101 insertions(+), 1250 deletions(-) rename src/{ => lib}/completions.js (61%) rename src/{ => lib}/constants.js (100%) rename src/{ => lib}/debug.js (100%) create mode 100644 src/lib/editor-config.js create mode 100644 src/lib/schema-validation.js rename src/{ => lib}/share.js (92%) delete mode 100644 src/lib/utils.js create mode 100644 src/lib/utils.ts delete mode 100644 src/schema.js create mode 100644 src/templates/example-landofile.yml diff --git a/src/components/ui/alert-dialog.jsx b/src/components/ui/alert-dialog.jsx index 33af045..923ff67 100644 --- a/src/components/ui/alert-dialog.jsx +++ b/src/components/ui/alert-dialog.jsx @@ -4,12 +4,40 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" +/** + * AlertDialog component. + * + * This component wraps the AlertDialogPrimitive.Root component from @radix-ui/react-alert-dialog. + * It is used to create a dialog that can be triggered to open or close. + */ const AlertDialog = AlertDialogPrimitive.Root +/** + * AlertDialogTrigger component. + * + * This component wraps the AlertDialogPrimitive.Trigger component from @radix-ui/react-alert-dialog. + * It is used to trigger the opening of the AlertDialog. + */ const AlertDialogTrigger = AlertDialogPrimitive.Trigger +/** + * AlertDialogPortal component. + * + * This component wraps the AlertDialogPrimitive.Portal component from @radix-ui/react-alert-dialog. + * It is used to portal the AlertDialogContent to the end of the document. + */ const AlertDialogPortal = AlertDialogPrimitive.Portal +/** + * AlertDialogOverlay component. + * + * This component wraps the AlertDialogPrimitive.Overlay component from @radix-ui/react-alert-dialog. + * It is used to create the overlay that covers the background when the AlertDialog is open. + * + * @param {Object} props - The props passed to the component. + * @param {React.RefObject} ref - The ref object passed to the component. + * @returns The AlertDialogOverlay component. + */ const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => ( ( )) AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName +/** + * AlertDialogContent component. + * + * This component wraps the AlertDialogPrimitive.Content component from @radix-ui/react-alert-dialog. + * It is used to create the content of the AlertDialog. + * + * @param {Object} props - The props passed to the component. + * @param {React.RefObject} ref - The ref object passed to the component. + * @returns The AlertDialogContent component. + */ const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => ( @@ -35,6 +73,14 @@ const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => ( )) AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName +/** + * AlertDialogHeader component. + * + * This component is used to create the header of the AlertDialog. + * + * @param {Object} props - The props passed to the component. + * @returns The AlertDialogHeader component. + */ const AlertDialogHeader = ({ className, ...props @@ -45,6 +91,14 @@ const AlertDialogHeader = ({ ) AlertDialogHeader.displayName = "AlertDialogHeader" +/** + * AlertDialogFooter component. + * + * This component is used to create the footer of the AlertDialog. + * + * @param {Object} props - The props passed to the component. + * @returns The AlertDialogFooter component. + */ const AlertDialogFooter = ({ className, ...props @@ -55,11 +109,31 @@ const AlertDialogFooter = ({ ) AlertDialogFooter.displayName = "AlertDialogFooter" +/** + * AlertDialogTitle component. + * + * This component wraps the AlertDialogPrimitive.Title component from @radix-ui/react-alert-dialog. + * It is used to create the title of the AlertDialog. + * + * @param {Object} props - The props passed to the component. + * @param {React.RefObject} ref - The ref object passed to the component. + * @returns The AlertDialogTitle component. + */ const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => ( )) AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName +/** + * AlertDialogDescription component. + * + * This component wraps the AlertDialogPrimitive.Description component from @radix-ui/react-alert-dialog. + * It is used to create the description of the AlertDialog. + * + * @param {Object} props - The props passed to the component. + * @param {React.RefObject} ref - The ref object passed to the component. + * @returns The AlertDialogDescription component. + */ const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => ( ( )) AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName +/** + * AlertDialogCancel component. + * + * This component wraps the AlertDialogPrimitive.Cancel component from @radix-ui/react-alert-dialog. + * It is used to create a cancel button within the AlertDialog. + * + * @param {Object} props - The props passed to the component. + * @param {React.RefObject} ref - The ref object passed to the component. + * @returns The AlertDialogCancel component. + */ const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => ( { const Comp = asChild ? Slot : "button" return ( diff --git a/src/components/ui/dialog.jsx b/src/components/ui/dialog.jsx index f530cde..072cd83 100644 --- a/src/components/ui/dialog.jsx +++ b/src/components/ui/dialog.jsx @@ -4,14 +4,48 @@ import { X } from "lucide-react" import { cn } from "@/lib/utils" +/** + * Dialog component. + * + * This component wraps the DialogPrimitive.Root component from @radix-ui/react-dialog. + * It is used to create a dialog that can be triggered to open or close. + */ const Dialog = DialogPrimitive.Root +/** + * DialogTrigger component. + * + * This component wraps the DialogPrimitive.Trigger component from @radix-ui/react-dialog. + * It is used to trigger the opening of the Dialog. + */ const DialogTrigger = DialogPrimitive.Trigger +/** + * DialogPortal component. + * + * This component wraps the DialogPrimitive.Portal component from @radix-ui/react-dialog. + * It is used to portal the DialogContent to the end of the document. + */ const DialogPortal = DialogPrimitive.Portal +/** + * DialogClose component. + * + * This component wraps the DialogPrimitive.Close component from @radix-ui/react-dialog. + * It is used to close the Dialog. + */ const DialogClose = DialogPrimitive.Close +/** + * DialogOverlay component. + * + * This component wraps the DialogPrimitive.Overlay component from @radix-ui/react-dialog. + * It is used to create the overlay that covers the background when the Dialog is open. + * + * @param {Object} props - The props passed to the component. + * @param {React.RefObject} ref - The ref object passed to the component. + * @returns The DialogOverlay component. + */ const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => ( ( )) DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +/** + * DialogContent component. + * + * This component wraps the DialogPrimitive.Content component from @radix-ui/react-dialog. + * It is used to create the content of the Dialog. + * + * @param {Object} props - The props passed to the component. + * @param {React.RefObject} ref - The ref object passed to the component. + * @returns The DialogContent component. + */ const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => ( @@ -44,6 +88,14 @@ const DialogContent = React.forwardRef(({ className, children, ...props }, ref) )) DialogContent.displayName = DialogPrimitive.Content.displayName +/** + * DialogHeader component. + * + * This component is used to create the header of the Dialog. + * + * @param {Object} props - The props passed to the component. + * @returns The DialogHeader component. + */ const DialogHeader = ({ className, ...props @@ -57,6 +109,14 @@ const DialogHeader = ({ ) DialogHeader.displayName = "DialogHeader" +/** + * DialogFooter component. + * + * This component is used to create the footer of the Dialog. + * + * @param {Object} props - The props passed to the component. + * @returns The DialogFooter component. + */ const DialogFooter = ({ className, ...props @@ -70,6 +130,16 @@ const DialogFooter = ({ ) DialogFooter.displayName = "DialogFooter" +/** + * DialogTitle component. + * + * This component wraps the DialogPrimitive.Title component from @radix-ui/react-dialog. + * It is used to create the title of the Dialog. + * + * @param {Object} props - The props passed to the component. + * @param {React.RefObject} ref - The ref object passed to the component. + * @returns The DialogTitle component. + */ const DialogTitle = React.forwardRef(({ className, ...props }, ref) => ( ( )) DialogTitle.displayName = DialogPrimitive.Title.displayName +/** + * DialogDescription component. + * + * This component wraps the DialogPrimitive.Description component from @radix-ui/react-dialog. + * It is used to create the description of the Dialog. + * + * @param {Object} props - The props passed to the component. + * @param {React.RefObject} ref - The ref object passed to the component. + * @returns The DialogDescription component. + */ const DialogDescription = React.forwardRef(({ className, ...props }, ref) => ( { @@ -19,22 +19,23 @@ export function EditorMenu({ editor, toast, isOpen, onToggle }) { showShareDialog(shareUrl); onToggle(false); } catch (error) { - debug.error('Failed to share:', error); + debug.error("Failed to share:", error); toast({ - description: 'Failed to generate share URL', + description: "Failed to generate share URL", duration: 5000, - className: 'bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-200', + className: + "bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-200", }); } }; const handleSave = () => { const content = editor.getValue(); - const blob = new Blob([content], { type: 'text/yaml' }); + const blob = new Blob([content], { type: "text/yaml" }); const url = URL.createObjectURL(blob); - const a = document.createElement('a'); + const a = document.createElement("a"); a.href = url; - a.download = '_.lando.yml'; + a.download = "_.lando.yml"; document.body.appendChild(a); a.click(); document.body.removeChild(a); @@ -42,7 +43,8 @@ export function EditorMenu({ editor, toast, isOpen, onToggle }) { onToggle(false); toast({ - description: 'Remember to remove the underscore from "_.lando.yml" after downloading', + description: + 'Remember to remove the underscore from "_.lando.yml" after downloading', duration: 5000, }); }; @@ -52,11 +54,12 @@ export function EditorMenu({ editor, toast, isOpen, onToggle }) { if (!file) return; if (!file.name.match(/^\.lando(\..*)?\.yml$/i)) { - debug.warn('Invalid file type:', file.name); + debug.warn("Invalid file type:", file.name); toast({ - description: 'Only .lando.yml and .lando.*.yml files are supported', + description: "Only .lando.yml and .lando.*.yml files are supported", duration: 5000, - className: 'bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-200', + className: + "bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-200", }); return; } @@ -66,36 +69,56 @@ export function EditorMenu({ editor, toast, isOpen, onToggle }) { const formatted = formatYaml(content); if (formatted !== content) { toast({ - description: 'File was automatically formatted', + description: "File was automatically formatted", duration: 2000, }); } - editor.executeEdits('format', [{ - range: editor.getModel().getFullModelRange(), - text: formatted, - }]); - debug.log('File loaded successfully:', file.name); + editor.executeEdits("format", [ + { + range: editor.getModel().getFullModelRange(), + text: formatted, + }, + ]); + debug.log("File loaded successfully:", file.name); onToggle(false); } catch (error) { - debug.error('Error reading file:', error); - monaco.editor.setModelMarkers(editor.getModel(), 'yaml', [{ - severity: MarkerSeverity.Error, - message: `Error reading file: ${error.message}`, - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 1, - }]); + debug.error("Error reading file:", error); + monaco.editor.setModelMarkers(editor.getModel(), "yaml", [ + { + severity: MarkerSeverity.Error, + message: `Error reading file: ${error.message}`, + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1, + }, + ]); } }; return ( -
+
diff --git a/src/components/ui/editor.jsx b/src/components/ui/editor.jsx index ba46794..0854b59 100644 --- a/src/components/ui/editor.jsx +++ b/src/components/ui/editor.jsx @@ -1,42 +1,95 @@ -import React, { useEffect, useRef, useState, useLayoutEffect } from 'react'; -import * as monaco from 'monaco-editor'; -import { TokenizationRegistry } from 'monaco-editor/esm/vs/editor/common/languages'; -import { MarkerSeverity } from 'monaco-editor/esm/vs/platform/markers/common/markers'; -import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; -import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; -import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution'; -import { loadSchema, validateYaml, getHoverInfo } from '../../schema'; -import { debug } from '../../debug'; -import { formatYaml, setupYamlFormatting } from '../../lib/format-yaml'; -import { useToast } from "@/hooks/use-toast" -import { generateShareUrl, getSharedContent } from '../../share'; -import { showShareDialog } from '../../lib/dialog'; -import { registerCompletionProvider } from '../../completions'; -import { EditorMenu } from './editor-menu'; -import { saveEditorContent, loadEditorContent } from '../../lib/storage'; - +import React, { useEffect, useRef, useState } from "react"; +import * as monaco from "monaco-editor"; +import { MarkerSeverity } from "monaco-editor/esm/vs/platform/markers/common/markers"; +import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; +import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; +import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution"; +import { setupEditorFeatures } from "@/lib/schema-validation"; +import { debug } from "@/lib/debug"; +import { formatYaml } from "@/lib/format-yaml"; +import { useToast } from "@/hooks/use-toast"; +import { getSharedContent } from "@/lib/share"; +import { EditorMenu } from "./editor-menu"; +import { saveEditorContent } from "@/lib/storage"; +import { getEditorOptions } from "@/lib/editor-config"; + +/** + * @fileoverview + * Monaco Editor component for Lando configuration files. + * This component provides a full-featured YAML editor with Lando-specific functionality, + * including schema validation, autocompletion, and custom formatting. + * It handles file drag-and-drop, content sharing, and local storage persistence. + */ + +/** + * Configures Monaco editor web workers for JSON and general editor functionality + */ +const setupMonacoWorkers = () => { + debug.log("Setting up Monaco workers"); + self.MonacoEnvironment = { + getWorker(_, label) { + debug.log("Requesting worker for language:", label); + if (label === "json") { + return new jsonWorker(); + } + return new editorWorker(); + }, + }; +}; + +/** + * A Monaco-based YAML editor component specialized for Lando configuration files. + * Provides features including: + * - Syntax highlighting + * - Schema validation + * - Auto-completion + * - File drag & drop + * - Content sharing + * - Local storage persistence + * - Auto-formatting + * + * @returns {JSX.Element} The editor component + */ export function Editor() { + /** + * Reference to the DOM container element for the Monaco editor + * @type {React.RefObject} + */ const containerRef = useRef(null); + + /** + * Reference to the Monaco editor instance + * @type {React.RefObject} + */ const editorRef = useRef(null); + + /** + * Flag to prevent multiple editor initializations + * @type {React.RefObject} + */ const initializingRef = useRef(false); + + /** + * Toast notification hook for displaying user feedback + */ const { toast } = useToast(); + + /** + * State for controlling the editor menu visibility + * @type {[boolean, React.Dispatch>]} + */ const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isEditorReady, setIsEditorReady] = useState(false); - // Setup Monaco workers - const setupMonacoWorkers = () => { - debug.log('Setting up Monaco workers'); - self.MonacoEnvironment = { - getWorker(_, label) { - debug.log('Requesting worker for language:', label); - if (label === 'json') { - return new jsonWorker(); - } - return new editorWorker(); - }, - }; - }; + /** + * State indicating whether the editor has completed initialization + * @type {[boolean, React.Dispatch>]} + */ + const [isEditorReady, setIsEditorReady] = useState(false); + /** + * Effect hook for initializing the Monaco editor + * Sets up the editor instance, workers, and features + */ useEffect(() => { // Wait for next frame to ensure container is ready requestAnimationFrame(() => { @@ -45,13 +98,16 @@ export function Editor() { initializingRef.current = true; try { - debug.log('Initializing editor...'); + debug.log("Initializing editor..."); if (!containerRef.current) { - throw new Error('Editor container not found'); + throw new Error("Editor container not found"); } setupMonacoWorkers(); - editorRef.current = monaco.editor.create(containerRef.current, getEditorOptions()); + editorRef.current = monaco.editor.create( + containerRef.current, + getEditorOptions(), + ); await setupEditorFeatures(); setupEventListeners(); handleSharedContent(); @@ -59,16 +115,16 @@ export function Editor() { // Fade out loader setTimeout(() => { - const loader = document.getElementById('editor-loader'); + const loader = document.getElementById("editor-loader"); if (loader) { - loader.style.opacity = '0'; - loader.addEventListener('transitionend', () => loader.remove()); + loader.style.opacity = "0"; + loader.addEventListener("transitionend", () => loader.remove()); } }, 500); - debug.log('Editor initialized successfully'); + debug.log("Editor initialized successfully"); } catch (error) { - debug.error('Failed to initialize editor:', error); + debug.error("Failed to initialize editor:", error); throw error; } }; @@ -83,193 +139,61 @@ export function Editor() { }; }, []); + /** + * Effect hook for handling clicks outside the editor menu + * Closes the menu when clicking outside its boundaries + */ useEffect(() => { const handleClickOutside = (e) => { - if (isMenuOpen && !e.target.closest('.editor-drawer') && !e.target.closest('#editor-menu-button')) { + if ( + isMenuOpen && + !e.target.closest(".editor-drawer") && + !e.target.closest("#editor-menu-button") + ) { setIsMenuOpen(false); } }; - document.addEventListener('click', handleClickOutside); - return () => document.removeEventListener('click', handleClickOutside); + document.addEventListener("click", handleClickOutside); + return () => document.removeEventListener("click", handleClickOutside); }, [isMenuOpen]); - const getEditorOptions = () => ({ - value: getDefaultContent(), - language: 'yaml', - theme: document.documentElement.classList.contains('dark') ? 'lando' : 'vs', - automaticLayout: true, - minimap: { enabled: false }, - scrollBeyondLastLine: false, - fontSize: 18, - lineNumbers: 'on', - renderWhitespace: 'selection', - tabSize: 2, - fixedOverflowWidgets: true, - quickSuggestions: true, - suggestOnTriggerCharacters: true, - wordBasedSuggestions: false, - parameterHints: { enabled: true }, - suggest: { - snippetsPreventQuickSuggestions: false, - showWords: false, - filterGraceful: false, - showSnippets: true, - showProperties: true, - localityBonus: true, - insertMode: 'insert', - insertHighlight: true, - selectionMode: 'always', - }, - acceptSuggestionOnEnter: 'on', - acceptSuggestionOnCommitCharacter: true, - snippetSuggestions: 'inline', - tabCompletion: 'on', - snippetOptions: { exitOnEnter: true }, - }); - - const setupEditorFeatures = async () => { - // Set up YAML tokenization - const existingTokensProvider = TokenizationRegistry.get('yaml'); - if (existingTokensProvider) { - const originalTokenize = existingTokensProvider.tokenize.bind(existingTokensProvider); - monaco.languages.setMonarchTokensProvider('yaml', { - ...existingTokensProvider, - tokenize: (line, state) => { - const tokens = originalTokenize(line, state); - if (tokens && tokens.tokens) { - tokens.tokens = tokens.tokens.map(token => { - if (token.scopes.includes('type.yaml')) { - return { ...token, scopes: ['key'] }; - } - return token; - }); - } - return tokens; - }, - }); - } - - // Register hover provider - monaco.languages.registerHoverProvider('yaml', { - provideHover: (model, position) => { - const content = model.getValue(); - return getHoverInfo(content, position); - } - }); - - // Load schema and set up validation - debug.log('Loading schema...'); - const schemaContent = await loadSchema(); - - if (!schemaContent) { - handleSchemaLoadFailure(); - } else { - debug.log('Schema loaded successfully'); - setupSchemaValidation(schemaContent); - } - - // Register YAML completion provider - if (schemaContent) { - registerCompletionProvider(schemaContent); - } - - // Add format action setup - setupYamlFormatting(editorRef.current, toast); - }; - - const handleSchemaLoadFailure = () => { - debug.warn('Schema failed to load - editor will continue without validation'); - monaco.editor.setModelMarkers(editorRef.current.getModel(), 'yaml', [{ - severity: MarkerSeverity.Warning, - message: 'Schema validation unavailable - schema failed to load', - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 1, - }]); - }; - - const setupSchemaValidation = (schemaContent) => { - const updateDiagnostics = () => { - const content = editorRef.current.getValue(); - try { - debug.log('Validating YAML content...'); - const diagnostics = validateYaml(content, schemaContent); - debug.log('Validation results:', diagnostics); - monaco.editor.setModelMarkers(editorRef.current.getModel(), 'yaml', diagnostics); - } catch (e) { - debug.error('Validation error:', e); - } - }; - - // Update diagnostics on content change - editorRef.current.onDidChangeModelContent(() => { - debug.log('Content changed, updating diagnostics...'); - updateDiagnostics(); - }); - - // Initial validation - updateDiagnostics(); - }; - + /** + * Handles shared content from URL parameters + * Loads the content into the editor and shows a notification + */ const handleSharedContent = () => { const sharedContent = getSharedContent(); if (sharedContent) { try { editorRef.current.setValue(sharedContent); toast({ - description: 'Loaded shared Landofile', + description: "Loaded shared Landofile", duration: 2000, }); // Clear the URL parameter without reloading the page const url = new URL(window.location.href); - url.searchParams.delete('s'); - window.history.replaceState({}, '', url.toString()); + url.searchParams.delete("s"); + window.history.replaceState({}, "", url.toString()); } catch (error) { - debug.error('Failed to load shared content:', error); + debug.error("Failed to load shared content:", error); toast({ - description: 'Failed to load shared content', + description: "Failed to load shared content", duration: 5000, - className: 'bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-200', + className: + "bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-200", }); } } }; - const getDefaultContent = () => { - // First try to load shared content - const sharedContent = getSharedContent(); - if (sharedContent) { - return sharedContent; - } - - // Then try to load from local storage - const savedContent = loadEditorContent(); - if (savedContent) { - return savedContent; - } - - // Fall back to default template - return `name: my-lando-app -recipe: lamp -config: - php: '8.3' - webroot: . - database: mysql:8.0 - xdebug: false - -services: - node: - type: node:20 - build: - - npm install - command: vite --host 0.0.0.0 - port: 5173 - ssl: true -`; - }; - + /** + * Sets up event listeners for the editor including: + * - Paste formatting + * - Window resize handling + * - File drag and drop + * - Content change persistence + */ const setupEventListeners = () => { // Handle paste events editorRef.current.onDidPaste(() => { @@ -279,60 +203,65 @@ services: // Only format and show toast if content changed if (formatted !== content) { - editorRef.current.executeEdits('format', [{ - range: editorRef.current.getModel().getFullModelRange(), - text: formatted, - }]); + editorRef.current.executeEdits("format", [ + { + range: editorRef.current.getModel().getFullModelRange(), + text: formatted, + }, + ]); toast({ - description: 'Content was automatically formatted', + description: "Content was automatically formatted", duration: 2000, }); } } catch (error) { - debug.error('Failed to format pasted content:', error); + debug.error("Failed to format pasted content:", error); toast({ description: `Failed to format: ${error.message}`, duration: 5000, - className: 'bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-200', + className: + "bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-200", }); } }); // Handle window resizing - window.addEventListener('resize', () => { - debug.log('Window resized, updating editor layout...'); + window.addEventListener("resize", () => { + debug.log("Window resized, updating editor layout..."); editorRef.current.layout(); }); // Update drag and drop handling to target the main container const editorContainer = containerRef.current; if (editorContainer) { - editorContainer.addEventListener('dragover', (e) => { + editorContainer.addEventListener("dragover", (e) => { e.preventDefault(); - editorContainer.classList.add('drag-over'); + editorContainer.classList.add("drag-over"); }); - editorContainer.addEventListener('dragleave', () => { - editorContainer.classList.remove('drag-over'); + editorContainer.addEventListener("dragleave", () => { + editorContainer.classList.remove("drag-over"); }); - editorContainer.addEventListener('drop', async (e) => { + editorContainer.addEventListener("drop", async (e) => { e.preventDefault(); - editorContainer.classList.remove('drag-over'); + editorContainer.classList.remove("drag-over"); const file = e.dataTransfer.files[0]; if (!file) return; if (!file.name.match(/^\.lando(\..*)?\.yml$/i)) { - debug.warn('Invalid file type:', file.name); - monaco.editor.setModelMarkers(editorRef.current.getModel(), 'yaml', [{ - severity: MarkerSeverity.Error, - message: 'Only .lando.yml and .lando.*.yml files are supported', - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 1, - }]); + debug.warn("Invalid file type:", file.name); + monaco.editor.setModelMarkers(editorRef.current.getModel(), "yaml", [ + { + severity: MarkerSeverity.Error, + message: "Only .lando.yml and .lando.*.yml files are supported", + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1, + }, + ]); return; } @@ -341,37 +270,42 @@ services: const formatted = formatYaml(content); if (formatted !== content) { toast({ - description: 'File was automatically formatted', + description: "File was automatically formatted", duration: 2000, }); } - editorRef.current.executeEdits('format', [{ - range: editorRef.current.getModel().getFullModelRange(), - text: formatted, - }]); - debug.log('File loaded successfully:', file.name); + editorRef.current.executeEdits("format", [ + { + range: editorRef.current.getModel().getFullModelRange(), + text: formatted, + }, + ]); + debug.log("File loaded successfully:", file.name); } catch (error) { - debug.error('Error reading file:', error); + debug.error("Error reading file:", error); toast({ description: `Error reading file: ${error.message}`, duration: 5000, - className: 'bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-200', + className: + "bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-200", }); - monaco.editor.setModelMarkers(editorRef.current.getModel(), 'yaml', [{ - severity: MarkerSeverity.Error, - message: `Error reading file: ${error.message}`, - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 1, - }]); + monaco.editor.setModelMarkers(editorRef.current.getModel(), "yaml", [ + { + severity: MarkerSeverity.Error, + message: `Error reading file: ${error.message}`, + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1, + }, + ]); } }); } // Add content change listener to save to localStorage editorRef.current.onDidChangeModelContent(() => { - debug.log('Content changed, saving to localStorage...'); + debug.log("Content changed, saving to localStorage..."); const content = editorRef.current.getValue(); saveEditorContent(content); // Existing validation call @@ -384,15 +318,29 @@ services:
{editorRef.current && ( diff --git a/src/components/ui/input.jsx b/src/components/ui/input.jsx index 70831a4..369a94c 100644 --- a/src/components/ui/input.jsx +++ b/src/components/ui/input.jsx @@ -1,7 +1,18 @@ import * as React from "react" - import { cn } from "@/lib/utils" +/** + * Input component. + * + * This component renders an input field with a default set of styles. + * It uses the cn utility function from @/lib/utils to concatenate the base and variant class names. + * + * @param {Object} props - The props passed to the component. + * @param {String} props.className - Additional class names to be applied to the component. + * @param {String} props.type - The type of the input field. + * @param {React.RefObject} ref - The ref object passed to the component. + * @returns {React.ReactElement} - The Input component. + */ const Input = React.forwardRef(({ className, type, ...props }, ref) => { return ( ( e.target.select()} />