diff --git a/.changeset/tame-turtles-obey.md b/.changeset/tame-turtles-obey.md new file mode 100644 index 0000000..ed98579 --- /dev/null +++ b/.changeset/tame-turtles-obey.md @@ -0,0 +1,5 @@ +--- +"sqlseal": minor +--- + +reworked JSONViewer to allow for visual JSONPath preview diff --git a/src/modules/settings/init.ts b/src/modules/settings/init.ts index 7d98168..9f034e7 100644 --- a/src/modules/settings/init.ts +++ b/src/modules/settings/init.ts @@ -17,7 +17,7 @@ export class SettingsInit { viewPluginGenerator: ViewPluginGeneratorType ) { const csvControl = new SettingsCSVControls(settings, app, plugin, viewPluginGenerator); - const jsonControl = new SettingsJsonControls(settings, app, plugin); + const jsonControl = new SettingsJsonControls(settings, app, plugin, viewPluginGenerator); const controls = [csvControl, jsonControl]; diff --git a/src/modules/settings/modal/showCodeSample.ts b/src/modules/settings/modal/showCodeSample.ts index 4daf87f..d46d9de 100644 --- a/src/modules/settings/modal/showCodeSample.ts +++ b/src/modules/settings/modal/showCodeSample.ts @@ -11,8 +11,18 @@ SELECT * FROM ${tableName} LIMIT 100 \`\`\`` +const jsonQuery = (tableName: string, path: string, jsonPath: string) => { + const escapedJsonPath = jsonPath.replace(/"/g, '\\"'); + return `\`\`\`sqlseal +TABLE ${tableName} = file(${path}, "${escapedJsonPath}") + +SELECT * FROM ${tableName} +LIMIT 100 +\`\`\``; +} + export class CodeSampleModal extends Modal { - constructor(app: App, private file: TFile, private viewPluginGenerator: ViewPluginGeneratorType) { + constructor(app: App, private file: TFile, private viewPluginGenerator: ViewPluginGeneratorType, private jsonPath?: string) { super(app); } @@ -24,7 +34,10 @@ export class CodeSampleModal extends Modal { // Setup actual editor here const tableName = sanitise(this.file.basename) - const q = query(tableName, this.file.path) + const isJsonFile = this.file.extension === 'json' || this.file.extension === 'json5'; + const q = isJsonFile && this.jsonPath + ? jsonQuery(tableName, this.file.path, this.jsonPath) + : query(tableName, this.file.path) const state = EditorState.create({ doc: q, diff --git a/src/modules/settings/settingsTabSection/SettingsJsonControls.ts b/src/modules/settings/settingsTabSection/SettingsJsonControls.ts index b3965aa..5d2d47c 100644 --- a/src/modules/settings/settingsTabSection/SettingsJsonControls.ts +++ b/src/modules/settings/settingsTabSection/SettingsJsonControls.ts @@ -1,4 +1,4 @@ -import { App, Setting } from "obsidian"; +import { App, Plugin, Setting } from "obsidian"; import { Settings } from "../Settings"; import { SettingsControls } from "./SettingsControls"; import { checkTypeViewAvaiability } from "../utils/viewInspector"; @@ -7,10 +7,15 @@ import { JSON_VIEW_TYPE, JsonView, } from "../view/JsonView"; +import { ViewPluginGeneratorType } from "../../syntaxHighlight/viewPluginGenerator"; export class SettingsJsonControls extends SettingsControls { private registeredView: string | null = null; + constructor(settings: Settings, app: App, plugin: Plugin, private viewPluginGenerator: ViewPluginGeneratorType) { + super(settings, app, plugin); + } + register() { if (this.settings.get("enableJSONViewer")) { const view = checkTypeViewAvaiability(this.app, JSON_VIEW_EXTENSIONS[0]); @@ -20,7 +25,7 @@ export class SettingsJsonControls extends SettingsControls { return; } - this.plugin.registerView(JSON_VIEW_TYPE, (leaf) => new JsonView(leaf)); + this.plugin.registerView(JSON_VIEW_TYPE, (leaf) => new JsonView(leaf, this.viewPluginGenerator)); this.plugin.registerExtensions(JSON_VIEW_EXTENSIONS, JSON_VIEW_TYPE); } } diff --git a/src/modules/settings/view/JsonView.ts b/src/modules/settings/view/JsonView.ts index 7b67944..baf9879 100644 --- a/src/modules/settings/view/JsonView.ts +++ b/src/modules/settings/view/JsonView.ts @@ -1,14 +1,253 @@ -import { WorkspaceLeaf, TextFileView, Menu, IconName } from 'obsidian'; -import { parse, stringify } from 'json5' +import { WorkspaceLeaf, TextFileView, Menu, IconName, Notice, ButtonComponent } from 'obsidian'; +import { parse, stringify } from 'json5'; +import * as jsonpath from 'jsonpath'; +import { uniq } from 'lodash'; +import { CodeSampleModal } from '../modal/showCodeSample'; +import { ViewPluginGeneratorType } from '../../syntaxHighlight/viewPluginGenerator'; export const JSON_VIEW_TYPE = "sqlseal-json-viewer"; export const JSON_VIEW_EXTENSIONS = ['json', 'json5']; +class JsonRenderer { + applySyntaxHighlighting(codeElement: HTMLElement): void { + const text = codeElement.textContent || ''; + codeElement.innerHTML = text + .replace(/"([^"]+)":/g, '"$1":') + .replace(/: (true|false)/g, ': $1') + .replace(/: (null)/g, ': $1') + .replace(/: (-?\d+(?:\.\d+)?)/g, ': $1'); + } + + renderFormattedJson(container: HTMLElement, parsedData: any): void { + container.empty(); + const pre = container.createEl('pre'); + const code = pre.createEl('code', { cls: 'language-json' }); + + // Enable text selection like a textarea + pre.style.userSelect = 'text'; + pre.style.cursor = 'text'; + code.style.userSelect = 'text'; + + const formatted = stringify(parsedData, null, 2); + if (formatted.length > 50000) { + const truncated = formatted.substring(0, 50000) + '\n\n... (content truncated for display)'; + code.textContent = truncated; + } else { + code.textContent = formatted; + } + + this.applySyntaxHighlighting(code); + } +} + +class TableRenderer { + processTablePreview( + container: HTMLElement, + parsedData: any, + jsonPath: string, + isLargeFile: boolean + ): void { + try { + let queryResult = parsedData; + + if (jsonPath !== '$') { + queryResult = jsonpath.query(parsedData, jsonPath); + } + + if (!Array.isArray(queryResult)) { + container.createEl('div', { + cls: 'sqlseal-info', + text: 'JSONPath result is not an array. Table preview requires array data.' + }); + return; + } + + if (queryResult.length === 0) { + container.createEl('div', { + cls: 'sqlseal-info', + text: 'No data matches the JSONPath query.' + }); + return; + } + + this.renderTable(container, queryResult, isLargeFile); + + } catch (e) { + const errorDiv = container.createEl('div', { cls: 'sqlseal-error' }); + + errorDiv.createEl('h4', { text: 'JSONPath Query Error' }); + + const errorMessage = e instanceof Error ? e.message : 'Invalid query syntax'; + errorDiv.createEl('p', { text: `Error: ${errorMessage}` }); + + // Add some helpful tips + const tipsDiv = errorDiv.createEl('div', { cls: 'sqlseal-error-tips' }); + tipsDiv.createEl('p', { text: 'Common JSONPath examples:' }); + const tipsList = tipsDiv.createEl('ul'); + tipsList.createEl('li', { text: '$ - Root object' }); + tipsList.createEl('li', { text: '$.users[*] - All items in users array' }); + tipsList.createEl('li', { text: '$.users[0] - First user' }); + tipsList.createEl('li', { text: '$.users[?(@.active)] - Users where active is true' }); + } + } + + private renderTable(container: HTMLElement, queryResult: any[], isLargeFile: boolean): void { + const maxRows = isLargeFile ? 50 : 100; + const displayRows = queryResult.slice(0, maxRows); + + const sampleSize = Math.min(displayRows.length, 20); + const columns = uniq(displayRows.slice(0, sampleSize).map(row => + typeof row === 'object' && row !== null ? Object.keys(row) : [] + ).flat()); + + if (columns.length === 0) { + container.createEl('div', { + cls: 'sqlseal-info', + text: 'Data contains no object properties to display in table.' + }); + return; + } + + const table = container.createEl('table', { cls: 'sqlseal-preview-table' }); + + this.createTableHeader(table, columns); + this.createTableBody(table, displayRows, columns); + this.addRowCountInfo(container, queryResult.length, maxRows); + } + + private createTableHeader(table: HTMLElement, columns: string[]): void { + const thead = table.createEl('thead'); + const headerRow = thead.createEl('tr'); + columns.forEach(col => { + headerRow.createEl('th', { text: col }); + }); + } + + private createTableBody(table: HTMLElement, displayRows: any[], columns: string[]): void { + const tbody = table.createEl('tbody'); + displayRows.forEach((row) => { + const tableRow = tbody.createEl('tr'); + columns.forEach(col => { + const cell = tableRow.createEl('td'); + const value = typeof row === 'object' && row !== null ? row[col] : ''; + if (value === null || value === undefined) { + cell.textContent = ''; + cell.addClass('sqlseal-null-cell'); + } else if (typeof value === 'object') { + cell.textContent = JSON.stringify(value); + cell.addClass('sqlseal-object-cell'); + } else { + cell.textContent = String(value); + } + }); + }); + } + + private addRowCountInfo(container: HTMLElement, totalRows: number, maxRows: number): void { + if (totalRows > maxRows) { + container.createEl('div', { + cls: 'sqlseal-row-info', + text: `Showing first ${maxRows} of ${totalRows} rows (limited for performance)` + }); + } else { + container.createEl('div', { + cls: 'sqlseal-row-info', + text: `${totalRows} rows` + }); + } + } +} + +class UIBuilder { + createWarningSection(container: HTMLElement, sizeInMB: number): HTMLButtonElement { + const warningSection = container.createDiv({ cls: 'sqlseal-file-warning' }); + warningSection.createEl('div', { + cls: 'sqlseal-warning-text', + text: `⚠️ Large file detected (~${sizeInMB}MB). Analysis disabled to prevent freezing.` + }); + + return warningSection.createEl('button', { + cls: 'sqlseal-analyze-btn', + text: 'Analyze File (may take time)' + }); + } + + createInputSection(container: HTMLElement, currentPath: string, isDisabled: boolean): HTMLInputElement { + const inputSection = container.createDiv({ cls: 'sqlseal-json-input-section' }); + inputSection.createEl('label', { text: 'JSONPath Query:' }); + return inputSection.createEl('input', { + type: 'text', + value: currentPath, + placeholder: '$.users[*] or $.data.items[?(@.active)]', + disabled: isDisabled + }); + } + + createContentPanels(container: HTMLElement): { + jsonContainer: HTMLElement; + tableContainer: HTMLElement; + } { + const contentArea = container.createDiv({ cls: 'sqlseal-json-content' }); + + const jsonPanel = contentArea.createDiv({ cls: 'sqlseal-json-panel' }); + const jsonContainer = jsonPanel.createDiv({ cls: 'sqlseal-json-display' }); + + const tablePanel = contentArea.createDiv({ cls: 'sqlseal-table-panel' }); + const tableContainer = tablePanel.createDiv({ cls: 'sqlseal-table-display' }); + + return { jsonContainer, tableContainer }; + } + + createGenerateCodeButton(container: HTMLElement, onGenerate: () => void): HTMLElement { + const buttonContainer = container.createDiv({ cls: 'sqlseal-button-container' }); + + const buttonComponent = new ButtonComponent(buttonContainer); + buttonComponent + .setButtonText('Generate SQLSeal Code') + .onClick(onGenerate); + + return buttonComponent.buttonEl; + } + + showPlaceholders(jsonContainer: HTMLElement, tableContainer: HTMLElement): void { + jsonContainer.createEl('div', { + cls: 'sqlseal-placeholder', + text: 'Click "Analyze File" above to parse and display the JSON content.' + }); + + tableContainer.createEl('div', { + cls: 'sqlseal-placeholder', + text: 'Table preview will be available after analysis.' + }); + } +} + +const LARGE_FILE_THRESHOLD = 1000000; + +function analyzeFileSize(content: string): { isLarge: boolean; sizeInMB: number } { + const isLarge = content.length > LARGE_FILE_THRESHOLD; + const sizeInMB = parseFloat((content.length / 1000000).toFixed(1)); + return { isLarge, sizeInMB }; +} + export class JsonView extends TextFileView { private content: string = ''; - private table: HTMLTableElement | undefined = undefined; + private parsedData: any = null; + private jsonPathInput: HTMLInputElement | undefined = undefined; + private jsonContainer: HTMLElement | undefined = undefined; + private tableContainer: HTMLElement | undefined = undefined; + private currentJsonPath: string = '$'; + private isLargeFile: boolean = false; + private isAnalyzed: boolean = false; + private analyzeButton: HTMLButtonElement | undefined = undefined; + private isAnalyzing: boolean = false; + private generateCodeButton: HTMLButtonElement | undefined = undefined; + + private jsonRenderer = new JsonRenderer(); + private tableRenderer = new TableRenderer(); + private uiBuilder = new UIBuilder(); - constructor(leaf: WorkspaceLeaf) { + constructor(leaf: WorkspaceLeaf, private readonly viewPluginGenerator: ViewPluginGeneratorType) { super(leaf); } @@ -26,6 +265,11 @@ export class JsonView extends TextFileView { } async onOpen() { + // Ensure we have fresh file data when reopening + if (this.file) { + const fileContent = await this.app.vault.cachedRead(this.file); + this.content = fileContent; + } this.renderJson(); } @@ -38,6 +282,9 @@ export class JsonView extends TextFileView { } async setViewData(data: string, clear: boolean): Promise { + if (clear) { + this.clear(); + } this.content = data; await this.renderJson(); } @@ -48,29 +295,155 @@ export class JsonView extends TextFileView { clear(): void { this.content = ''; - this.table?.empty(); + this.parsedData = null; + this.isLargeFile = false; + this.isAnalyzed = false; + this.isAnalyzing = false; + this.currentJsonPath = '$'; + this.jsonContainer?.empty(); + this.tableContainer?.empty(); + this.contentEl.empty(); } api: any = null; private async renderJson() { - if (!this.content) { - return + return; } - - this.contentEl.empty() - const csvEditorDiv = this.contentEl.createDiv({ cls: 'sqlseal-json-editor' }) - const code = csvEditorDiv.createEl('pre').createEl('code') - - try { - const data = parse(this.content) - const formatted = stringify(data, null, 2) - code.appendText(formatted) - } catch (e) { - console.error(e) - // EMPTY + this.contentEl.empty(); + + const { isLarge, sizeInMB } = analyzeFileSize(this.content); + this.isLargeFile = isLarge; + + const mainContainer = this.contentEl.createDiv({ cls: 'sqlseal-json-viewer' }); + + if (this.isLargeFile) { + this.analyzeButton = this.uiBuilder.createWarningSection(mainContainer, sizeInMB); + this.analyzeButton.addEventListener('click', () => { + this.performAnalysis(); + }); + } + + this.jsonPathInput = this.uiBuilder.createInputSection( + mainContainer, + this.currentJsonPath, + this.isLargeFile && !this.isAnalyzed + ); + + if (!this.isLargeFile || this.isAnalyzed) { + this.jsonPathInput.addEventListener('input', () => { + this.currentJsonPath = this.jsonPathInput!.value || '$'; + this.updateTablePreview(); + }); + } + + this.generateCodeButton = this.uiBuilder.createGenerateCodeButton(mainContainer, () => this.generateSQLSealCode()); + if (this.isLargeFile && !this.isAnalyzed) { + this.generateCodeButton.style.display = 'none'; + } + + const { jsonContainer, tableContainer } = this.uiBuilder.createContentPanels(mainContainer); + this.jsonContainer = jsonContainer; + this.tableContainer = tableContainer; + + if (!this.isLargeFile) { + await this.performAnalysis(); + } else { + this.uiBuilder.showPlaceholders(this.jsonContainer, this.tableContainer); } } + + private async performAnalysis() { + if (this.isAnalyzing || this.isAnalyzed) return; + + this.isAnalyzing = true; + + // Update button state + if (this.analyzeButton) { + this.analyzeButton.textContent = 'Analyzing...'; + this.analyzeButton.disabled = true; + } + + // Enable input + if (this.jsonPathInput) { + this.jsonPathInput.disabled = false; + this.jsonPathInput.addEventListener('input', () => { + this.currentJsonPath = this.jsonPathInput!.value || '$'; + this.updateTablePreview(); + }); + } + + try { + // Use setTimeout to allow UI to update + await new Promise(resolve => setTimeout(resolve, 100)); + + this.parsedData = parse(this.content); + this.isAnalyzed = true; + this.jsonRenderer.renderFormattedJson(this.jsonContainer!, this.parsedData); + this.updateTablePreview(); + + // Show generate code button for large files + if (this.generateCodeButton && this.isLargeFile) { + this.generateCodeButton.style.display = 'block'; + } + + // Hide warning section + const warningSection = this.contentEl.querySelector('.sqlseal-file-warning'); + if (warningSection) { + warningSection.remove(); + } + + } catch (e) { + console.error('Error parsing JSON:', e); + this.jsonContainer!.empty(); + this.jsonContainer!.createEl('div', { + cls: 'sqlseal-error', + text: `Error parsing JSON: ${e instanceof Error ? e.message : 'Unknown error'}` + }); + } finally { + this.isAnalyzing = false; + } + } + + private updateTablePreview() { + if (!this.tableContainer || !this.parsedData || this.isAnalyzing) return; + + this.tableContainer.empty(); + + if (this.isLargeFile) { + const loadingDiv = this.tableContainer.createEl('div', { + cls: 'sqlseal-loading', + text: 'Processing JSONPath query...' + }); + + setTimeout(() => { + loadingDiv.remove(); + this.tableRenderer.processTablePreview( + this.tableContainer!, + this.parsedData, + this.currentJsonPath, + this.isLargeFile + ); + }, 50); + } else { + this.tableRenderer.processTablePreview( + this.tableContainer, + this.parsedData, + this.currentJsonPath, + this.isLargeFile + ); + } + } + + private generateSQLSealCode(): void { + if (!this.file) { + new Notice('No file available for code generation'); + return; + } + + const modal = new CodeSampleModal(this.app, this.file, this.viewPluginGenerator, this.currentJsonPath); + modal.open(); + } } \ No newline at end of file diff --git a/src/modules/sync/syncStrategy/JSONFileSyncStrategy.ts b/src/modules/sync/syncStrategy/JSONFileSyncStrategy.ts index 37cd436..f1ab852 100644 --- a/src/modules/sync/syncStrategy/JSONFileSyncStrategy.ts +++ b/src/modules/sync/syncStrategy/JSONFileSyncStrategy.ts @@ -3,7 +3,7 @@ import { ISyncStrategy } from "./abstractSyncStrategy"; import { ParserTableDefinition } from "./types"; import { parse } from 'json5'; import { uniq } from "lodash"; -import * as jsonpath from 'jsonpath' +import * as jsonpath from 'jsonpath'; import { FilepathHasher } from "../../../utils/hasher"; const DEFAULT_FILE_HASH = '' diff --git a/src/styles/jsonViewer.scss b/src/styles/jsonViewer.scss new file mode 100644 index 0000000..8058077 --- /dev/null +++ b/src/styles/jsonViewer.scss @@ -0,0 +1,313 @@ +// JSON Viewer Styles +.sqlseal-json-viewer { + height: 100%; + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; + + .sqlseal-file-warning { + padding: 16px; + background: var(--background-modifier-cover); + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + display: flex; + flex-direction: column; + gap: 12px; + + .sqlseal-warning-text { + color: var(--text-warning); + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; + } + + .sqlseal-analyze-btn { + background: var(--interactive-accent); + color: var(--text-on-accent); + border: none; + padding: 10px 16px; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; + + &:hover:not(:disabled) { + background: var(--interactive-accent-hover); + } + + &:disabled { + background: var(--interactive-normal); + color: var(--text-muted); + cursor: not-allowed; + } + } + } + + .sqlseal-json-input-section { + display: flex; + flex-direction: column; + gap: 6px; + + label { + font-weight: 600; + color: var(--text-normal); + font-size: 14px; + } + + input { + padding: 8px 12px; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + background: var(--background-primary); + color: var(--text-normal); + font-family: var(--font-monospace); + font-size: 13px; + + &:focus { + outline: none; + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px var(--interactive-accent-hover); + } + + &::placeholder { + color: var(--text-muted); + } + } + } + + .sqlseal-button-container { + display: flex; + justify-content: flex-start; + + button { + width: auto; + min-width: 150px; + max-width: 200px; + } + } + + .sqlseal-json-content { + flex: 1; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + overflow: hidden; + } + + .sqlseal-json-panel, + .sqlseal-table-panel { + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + background: var(--background-primary); + + h3 { + margin: 0; + padding: 12px 16px; + border-bottom: 1px solid var(--background-modifier-border); + background: var(--background-secondary); + font-size: 14px; + font-weight: 600; + color: var(--text-normal); + } + } + + .sqlseal-json-display { + flex: 1; + overflow: auto; + padding: 16px; + + pre { + margin: 0; + font-family: var(--font-monospace); + font-size: 12px; + line-height: 1.4; + + code { + background: none; + padding: 0; + border-radius: 0; + + // JSON syntax highlighting + .json-key { + color: var(--color-blue); + font-weight: 600; + } + + .json-string { + color: var(--color-green); + } + + .json-number { + color: var(--color-orange); + } + + .json-boolean { + color: var(--color-purple); + } + + .json-null { + color: var(--text-muted); + font-style: italic; + } + } + } + } + + .sqlseal-table-display { + flex: 1; + overflow: auto; + padding: 16px; + + .sqlseal-preview-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + + th { + background: var(--background-secondary); + padding: 8px 12px; + text-align: left; + border-bottom: 2px solid var(--background-modifier-border); + font-weight: 600; + color: var(--text-normal); + position: sticky; + top: 0; + z-index: 10; + } + + td { + padding: 6px 12px; + border-bottom: 1px solid var(--background-modifier-border-hover); + color: var(--text-normal); + vertical-align: top; + + &.sqlseal-null-cell { + color: var(--text-muted); + font-style: italic; + } + + &.sqlseal-object-cell { + font-family: var(--font-monospace); + color: var(--text-accent); + font-size: 11px; + max-width: 200px; + word-break: break-all; + } + } + + tr:hover { + background: var(--background-secondary); + } + } + + .sqlseal-row-info { + margin-top: 12px; + padding: 8px; + background: var(--background-secondary); + border-radius: 4px; + font-size: 11px; + color: var(--text-muted); + text-align: center; + } + } + + .sqlseal-error, + .sqlseal-info, + .sqlseal-placeholder, + .sqlseal-loading { + padding: 12px; + border-radius: 6px; + font-size: 13px; + } + + .sqlseal-error { + background: var(--background-modifier-error); + color: white; + border: 1px solid var(--background-modifier-error-border); + + h4 { + margin: 0 0 8px 0; + color: white; + font-size: 14px; + } + + p { + margin: 4px 0; + color: white; + } + + .sqlseal-error-tips { + margin-top: 12px; + + p { + margin: 0 0 6px 0; + font-weight: 600; + } + + ul { + margin: 6px 0 0 16px; + padding: 0; + + li { + margin: 2px 0; + color: rgba(255, 255, 255, 0.9); + font-size: 12px; + font-family: var(--font-monospace); + } + } + } + } + + .sqlseal-info { + background: var(--background-modifier-cover); + color: var(--text-muted); + border: 1px solid var(--background-modifier-border); + } + + .sqlseal-placeholder { + background: var(--background-secondary); + color: var(--text-muted); + border: 1px solid var(--background-modifier-border); + text-align: center; + font-style: italic; + } + + .sqlseal-loading { + background: var(--background-modifier-cover); + color: var(--text-accent); + border: 1px solid var(--interactive-accent); + text-align: center; + position: relative; + + &::after { + content: ''; + display: inline-block; + width: 12px; + height: 12px; + margin-left: 8px; + border: 2px solid var(--text-accent); + border-top: 2px solid transparent; + border-radius: 50%; + animation: spin 1s linear infinite; + } + } +} + +// Loading spinner animation +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +// Responsive design for smaller screens +@media (max-width: 1000px) { + .sqlseal-json-viewer .sqlseal-json-content { + grid-template-columns: 1fr; + grid-template-rows: 300px 1fr; + } +} \ No newline at end of file diff --git a/src/styles/main.scss b/src/styles/main.scss index 357ab1e..0e37d02 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -9,5 +9,6 @@ @use 'canvas'; @use 'autocomplete'; @use 'global-tables'; +@use 'jsonViewer'; @use '../modules/explorer/explorer/style.scss'; \ No newline at end of file