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