From e7ffdbd1fb302bfb132be62c33b15026bb49477e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Dec 2025 16:50:36 +0000 Subject: [PATCH 1/9] Initial plan From edd1d3d3e5f6a211cc63d2e840c2ad296c225231 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:02:13 +0000 Subject: [PATCH 2/9] Add dark mode theme support with toggle button Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com> --- packages/yasgui/src/Tab.ts | 5 + packages/yasgui/src/TabElements.scss | 27 +++ packages/yasgui/src/TabElements.ts | 28 +++ packages/yasgui/src/ThemeManager.ts | 119 ++++++++++ packages/yasgui/src/defaults.ts | 2 + packages/yasgui/src/index.ts | 31 +++ packages/yasgui/src/themes.scss | 329 +++++++++++++++++++++++++++ 7 files changed, 541 insertions(+) create mode 100644 packages/yasgui/src/ThemeManager.ts create mode 100644 packages/yasgui/src/themes.scss diff --git a/packages/yasgui/src/Tab.ts b/packages/yasgui/src/Tab.ts index ae3dd5a2..758870df 100644 --- a/packages/yasgui/src/Tab.ts +++ b/packages/yasgui/src/Tab.ts @@ -356,8 +356,13 @@ export class Tab extends EventEmitter { } private initYasqe() { + // Set theme based on current yasgui theme + const currentTheme = this.yasgui.getTheme(); + const cmTheme = currentTheme === "dark" ? "material-palenight" : "default"; + const yasqeConf: Partial = { ...this.yasgui.config.yasqe, + theme: cmTheme, value: this.persistentJson.yasqe.value, editorHeight: this.persistentJson.yasqe.editorHeight ? this.persistentJson.yasqe.editorHeight : undefined, persistenceId: null, //yasgui handles persistent storing diff --git a/packages/yasgui/src/TabElements.scss b/packages/yasgui/src/TabElements.scss index 781bb2f3..b2e9f496 100644 --- a/packages/yasgui/src/TabElements.scss +++ b/packages/yasgui/src/TabElements.scss @@ -18,6 +18,33 @@ $minTabHeight: 35px; border-bottom: 2px solid transparent; box-sizing: border-box; } + .themeToggle { + cursor: pointer; + height: 100%; + margin-left: auto; + margin-right: 10px; + padding: 6px 8px; + background: transparent; + border: none; + color: var(--yasgui-button-text, #505050); + fill: var(--yasgui-button-text, #505050); + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + + &:hover { + color: var(--yasgui-button-hover, #000); + fill: var(--yasgui-button-hover, #000); + background: var(--yasgui-bg-secondary, #f7f7f7); + } + + svg { + width: 20px; + height: 20px; + } + } + .addTab { cursor: pointer; height: 100%; diff --git a/packages/yasgui/src/TabElements.ts b/packages/yasgui/src/TabElements.ts index 02cbcdfd..f3d51fe1 100644 --- a/packages/yasgui/src/TabElements.ts +++ b/packages/yasgui/src/TabElements.ts @@ -248,6 +248,20 @@ export class TabList { }, }); + // Add theme toggle button if enabled + if (this.yasgui.config.showThemeToggle) { + const themeToggleEl = document.createElement("button"); + themeToggleEl.className = "themeToggle"; + themeToggleEl.title = "Toggle theme"; + themeToggleEl.setAttribute("aria-label", "Toggle between light and dark theme"); + themeToggleEl.innerHTML = this.getThemeToggleIcon(); + themeToggleEl.addEventListener("click", () => { + this.yasgui.toggleTheme(); + themeToggleEl.innerHTML = this.getThemeToggleIcon(); + }); + this._tabsListEl.appendChild(themeToggleEl); + } + this.addTabEl = document.createElement("div"); this.addTabEl.setAttribute("role", "presentation"); @@ -321,6 +335,20 @@ export class TabList { this._tabsListEl?.insertBefore(this._tabs[tabId].draw(tabConf.name), this.addTabEl || null); } } + private getThemeToggleIcon(): string { + const currentTheme = this.yasgui.getTheme(); + if (currentTheme === "dark") { + // Moon icon for dark mode (clicking will switch to light) + return ` + + `; + } else { + // Sun icon for light mode (clicking will switch to dark) + return ` + + `; + } + } public destroy() { for (const tabId in this._tabs) { const tab = this._tabs[tabId]; diff --git a/packages/yasgui/src/ThemeManager.ts b/packages/yasgui/src/ThemeManager.ts new file mode 100644 index 00000000..73ba1c22 --- /dev/null +++ b/packages/yasgui/src/ThemeManager.ts @@ -0,0 +1,119 @@ +import { Storage as YStorage } from "@matdata/yasgui-utils"; + +export type Theme = "light" | "dark"; + +export class ThemeManager { + private static STORAGE_KEY = "yasgui_theme"; + private storage: YStorage; + private currentTheme: Theme; + private rootElement: HTMLElement; + + constructor(rootElement: HTMLElement) { + this.storage = new YStorage("yasgui"); + this.rootElement = rootElement; + this.currentTheme = this.getStoredTheme() || this.getSystemTheme(); + this.applyTheme(this.currentTheme); + } + + /** + * Get the currently active theme + */ + public getTheme(): Theme { + return this.currentTheme; + } + + /** + * Set and apply a new theme + */ + public setTheme(theme: Theme): void { + this.currentTheme = theme; + this.applyTheme(theme); + // Store theme preference with 1 year expiration + this.storage.set(ThemeManager.STORAGE_KEY, theme, 60 * 60 * 24 * 365, () => { + console.warn("Failed to store theme preference due to quota exceeded"); + }); + } + + /** + * Toggle between light and dark themes + */ + public toggleTheme(): Theme { + const newTheme = this.currentTheme === "light" ? "dark" : "light"; + this.setTheme(newTheme); + return newTheme; + } + + /** + * Apply theme to the DOM + */ + private applyTheme(theme: Theme): void { + // Set data-theme attribute on root element + this.rootElement.setAttribute("data-theme", theme); + + // Also set on document root for global styles + document.documentElement.setAttribute("data-theme", theme); + + // Update Yasqe CodeMirror theme + this.updateCodeMirrorTheme(theme); + } + + /** + * Update CodeMirror theme for all Yasqe editors + */ + private updateCodeMirrorTheme(theme: Theme): void { + const cmTheme = theme === "dark" ? "material-palenight" : "default"; + + // Find all CodeMirror instances within the root element and in document + const cmElements = document.querySelectorAll(".CodeMirror"); + cmElements.forEach((element) => { + const cm = (element as any).CodeMirror; + if (cm && cm.setOption) { + cm.setOption("theme", cmTheme); + } + }); + } + + /** + * Get theme from localStorage + */ + private getStoredTheme(): Theme | null { + const stored = this.storage.get(ThemeManager.STORAGE_KEY); + if (stored === "light" || stored === "dark") { + return stored; + } + return null; + } + + /** + * Detect system theme preference + */ + private getSystemTheme(): Theme { + if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { + return "dark"; + } + return "light"; + } + + /** + * Listen for system theme changes + */ + public listenToSystemTheme(): void { + if (window.matchMedia) { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + mediaQuery.addEventListener("change", (e) => { + // Only update if user hasn't manually set a theme + if (!this.storage.get(ThemeManager.STORAGE_KEY)) { + const newTheme = e.matches ? "dark" : "light"; + this.applyTheme(newTheme); + } + }); + } + } + + /** + * Refresh theme application (useful after new content is loaded) + */ + public refresh(): void { + this.applyTheme(this.currentTheme); + } +} diff --git a/packages/yasgui/src/defaults.ts b/packages/yasgui/src/defaults.ts index 5dd99dc7..02a54a83 100644 --- a/packages/yasgui/src/defaults.ts +++ b/packages/yasgui/src/defaults.ts @@ -28,6 +28,8 @@ export default function initialize(): Config { persistenceLabelConfig: "config", yasqe: Yasqe.defaults, yasr: Yasr.defaults, + theme: undefined, + showThemeToggle: true, endpointCatalogueOptions: { getData: () => { return [ diff --git a/packages/yasgui/src/index.ts b/packages/yasgui/src/index.ts index 17e46d70..03e1975b 100644 --- a/packages/yasgui/src/index.ts +++ b/packages/yasgui/src/index.ts @@ -12,8 +12,11 @@ import { default as Yasr, Config as YasrConfig } from "@matdata/yasr"; import { addClass, removeClass } from "@matdata/yasgui-utils"; import GeoPlugin from "yasgui-geo-tg"; import GraphPlugin from "@matdata/yasgui-graph-plugin"; +import { ThemeManager, Theme } from "./ThemeManager"; import "./index.scss"; +import "./themes.scss"; import "../../yasr/src/scss/global.scss"; +import "codemirror/theme/material-palenight.css"; // Register plugins to Yasr Yasr.registerPlugin("Geo", GeoPlugin); @@ -50,6 +53,8 @@ export interface Config { requestConfig: YasguiRequestConfig; contextMenuContainer: HTMLElement | undefined; nonSslDomain?: string; + theme?: Theme; + showThemeToggle?: boolean; } export type PartialConfig = { [P in keyof Config]?: Config[P] extends object ? Partial : Config[P]; @@ -96,6 +101,7 @@ export class Yasgui extends EventEmitter { public tabPanelsEl: HTMLDivElement; public config: Config; public persistentConfig: PersistentConfig; + public themeManager: ThemeManager; public static Tab = Tab; constructor(parent: HTMLElement, config: PartialConfig) { super(); @@ -104,6 +110,13 @@ export class Yasgui extends EventEmitter { parent.appendChild(this.rootEl); this.config = merge({}, Yasgui.defaults, config); + + // Initialize theme manager + this.themeManager = new ThemeManager(this.rootEl); + if (this.config.theme) { + this.themeManager.setTheme(this.config.theme); + } + this.themeManager.listenToSystemTheme(); this.persistentConfig = new PersistentConfig(this); this.tabElements = new TabElements(this); @@ -348,6 +361,24 @@ export class Yasgui extends EventEmitter { this.addTab(true, config.tab, { atIndex: config.index }); } } + /** + * Get the current theme + */ + public getTheme(): Theme { + return this.themeManager.getTheme(); + } + /** + * Set the theme + */ + public setTheme(theme: Theme): void { + this.themeManager.setTheme(theme); + } + /** + * Toggle between light and dark themes + */ + public toggleTheme(): Theme { + return this.themeManager.toggleTheme(); + } public destroy() { this.removeAllListeners(); this.tabElements.destroy(); diff --git a/packages/yasgui/src/themes.scss b/packages/yasgui/src/themes.scss new file mode 100644 index 00000000..22d9e298 --- /dev/null +++ b/packages/yasgui/src/themes.scss @@ -0,0 +1,329 @@ +// Theme variables and styles for Yasgui +// Supports light (default) and dark themes + +:root { + // Light theme colors (default) + --yasgui-bg-primary: #ffffff; + --yasgui-bg-secondary: #f7f7f7; + --yasgui-bg-tertiary: #eee; + --yasgui-text-primary: rgba(0, 0, 0, 0.87); + --yasgui-text-secondary: #505050; + --yasgui-text-muted: #999; + --yasgui-border-color: #ddd; + --yasgui-border-color-secondary: rgba(0, 0, 0, 0.26); + --yasgui-border-color-light: #d1d1d1; + --yasgui-link-color: #337ab7; + --yasgui-link-hover: #2a6496; + --yasgui-accent-color: #337ab7; + --yasgui-button-text: #505050; + --yasgui-button-hover: #000; + --yasgui-chip-bg: #f5f5f5; + --yasgui-chip-text: #505050; + --yasgui-tab-bg: #ffffff; + --yasgui-tab-bg-active: #f9f9f9; + --yasgui-tab-border: #ddd; + --yasgui-input-border: rgba(0, 0, 0, 0.26); + --yasgui-input-focus: #337ab7; + --yasgui-label-color: rgba(0, 0, 0, 0.54); + --yasgui-tooltip-bg: rgba(0, 0, 0, 0.8); + --yasgui-tooltip-text: #fff; + --yasgui-error-color: #d32f2f; + --yasgui-notification-bg: #eee; + --yasgui-notification-text: #999; + --yasgui-help-bg: #dff0ff; + --yasgui-help-text: #428bca; + --yasgui-fallback-bg: #f7f7f7; + --yasgui-fallback-border: #d1d1d1; + --yasgui-resize-handle: #d1d1d1; + --yasgui-nav-bg: #eee; +} + +[data-theme="dark"] { + // Dark theme colors + --yasgui-bg-primary: #1e1e1e; + --yasgui-bg-secondary: #252526; + --yasgui-bg-tertiary: #2d2d30; + --yasgui-text-primary: #d4d4d4; + --yasgui-text-secondary: #cccccc; + --yasgui-text-muted: #858585; + --yasgui-border-color: #3e3e42; + --yasgui-border-color-secondary: rgba(255, 255, 255, 0.15); + --yasgui-border-color-light: #3e3e42; + --yasgui-link-color: #4fc3f7; + --yasgui-link-hover: #81d4fa; + --yasgui-accent-color: #4fc3f7; + --yasgui-button-text: #cccccc; + --yasgui-button-hover: #ffffff; + --yasgui-chip-bg: #2d2d30; + --yasgui-chip-text: #cccccc; + --yasgui-tab-bg: #2d2d30; + --yasgui-tab-bg-active: #1e1e1e; + --yasgui-tab-border: #3e3e42; + --yasgui-input-border: rgba(255, 255, 255, 0.15); + --yasgui-input-focus: #4fc3f7; + --yasgui-label-color: rgba(255, 255, 255, 0.6); + --yasgui-tooltip-bg: rgba(55, 55, 55, 0.95); + --yasgui-tooltip-text: #d4d4d4; + --yasgui-error-color: #f48771; + --yasgui-notification-bg: #2d2d30; + --yasgui-notification-text: #858585; + --yasgui-help-bg: #1a4d5e; + --yasgui-help-text: #4fc3f7; + --yasgui-fallback-bg: #252526; + --yasgui-fallback-border: #3e3e42; + --yasgui-resize-handle: #3e3e42; + --yasgui-nav-bg: #2d2d30; +} + +// Apply theme colors to Yasgui components +.yasgui { + background-color: var(--yasgui-bg-primary); + color: var(--yasgui-text-primary); + + a { + color: var(--yasgui-link-color); + + &:hover, + &:active { + color: var(--yasgui-link-hover); + } + } + + // Tabs styling + .tabsList { + background: var(--yasgui-bg-primary); + border-bottom: 1px solid var(--yasgui-border-color); + } + + .tab { + background: var(--yasgui-tab-bg); + color: var(--yasgui-text-primary); + border-color: var(--yasgui-tab-border); + + &.active { + background: var(--yasgui-tab-bg-active); + border-bottom-color: var(--yasgui-tab-bg-active); + } + + &:hover:not(.active) { + background: var(--yasgui-bg-secondary); + } + } + + // Text fields + .yasgui_textfield { + > label { + color: var(--yasgui-label-color); + } + + > input, + > textarea { + color: var(--yasgui-text-primary); + border-bottom-color: var(--yasgui-input-border); + background-color: transparent; + + &:focus { + border-color: var(--yasgui-input-focus); + } + } + + > input:focus ~ label, + > textarea:focus ~ label { + color: var(--yasgui-input-focus); + } + } + + // Theme toggle button + .themeToggle { + border: none; + background: transparent; + cursor: pointer; + padding: 6px 8px; + color: var(--yasgui-button-text); + fill: var(--yasgui-button-text); + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + margin-left: 5px; + + &:hover { + color: var(--yasgui-button-hover); + fill: var(--yasgui-button-hover); + background: var(--yasgui-bg-secondary); + } + + svg { + width: 20px; + height: 20px; + } + } +} + +// Yasqe (editor) theming +.yasqe { + background-color: var(--yasgui-bg-primary); + + .CodeMirror { + background-color: var(--yasgui-bg-primary); + color: var(--yasgui-text-primary); + } + + .notification { + color: var(--yasgui-notification-text); + background-color: var(--yasgui-notification-bg); + } + + .yasqe_tooltip { + background: var(--yasgui-tooltip-bg); + color: var(--yasgui-tooltip-text); + } + + .resizeChip { + background-color: var(--yasgui-resize-handle); + } + + .parseErrorIcon svg g { + fill: var(--yasgui-error-color); + } +} + +// Yasr (results) theming +.yasr { + background-color: var(--yasgui-bg-primary); + color: var(--yasgui-text-primary); + + a { + color: var(--yasgui-link-color); + + &:hover, + &:active { + color: var(--yasgui-link-hover); + } + } + + .yasr_btn { + color: var(--yasgui-button-text); + fill: var(--yasgui-button-text); + + &.selected { + border-bottom-color: var(--yasgui-accent-color); + } + + &:not(.disabled):hover { + fill: var(--yasgui-button-hover); + color: var(--yasgui-button-hover); + } + } + + .yasr_response_chip { + color: var(--yasgui-chip-text); + background: var(--yasgui-chip-bg); + } + + .yasr_fallback_info:not(:empty) { + border-color: var(--yasgui-fallback-border); + background: var(--yasgui-fallback-bg); + } + + .yasr_help_variable { + background: var(--yasgui-help-bg); + color: var(--yasgui-help-text); + } + + &.fullscreen { + background: var(--yasgui-bg-primary); + } + + .yasr_loading_spinner { + border-color: var(--yasgui-bg-secondary); + border-top-color: var(--yasgui-accent-color); + } + + // Table plugin theming + .dataTable { + background-color: var(--yasgui-bg-primary); + color: var(--yasgui-text-primary); + + thead th { + background-color: var(--yasgui-bg-secondary); + color: var(--yasgui-text-primary); + border-bottom-color: var(--yasgui-border-color); + } + + tbody td { + border-color: var(--yasgui-border-color-light); + color: var(--yasgui-text-primary); + } + + tbody tr:hover { + background-color: var(--yasgui-bg-secondary); + } + } + + .tableControls { + input, + select { + background-color: var(--yasgui-bg-secondary); + color: var(--yasgui-text-primary); + border-color: var(--yasgui-input-border); + + &:focus { + border-color: var(--yasgui-input-focus); + } + } + } + + // Error plugin theming + .errorResult { + .errorMessageContainer { + .errorMessage { + color: var(--yasgui-text-primary) !important; + background-color: var(--yasgui-bg-secondary) !important; + border-color: var(--yasgui-border-color) !important; + } + } + + .redOutline { + color: var(--yasgui-error-color) !important; + background-color: var(--yasgui-bg-secondary) !important; + border-color: var(--yasgui-error-color) !important; + } + } + + // Response plugin theming + .cm-s-default { + background-color: var(--yasgui-bg-primary); + color: var(--yasgui-text-primary); + } +} + +// Modal theming +.modal { + .modal-content { + background-color: var(--yasgui-bg-primary); + color: var(--yasgui-text-primary); + border-color: var(--yasgui-border-color); + } + + .modal-header { + border-bottom-color: var(--yasgui-border-color); + } + + .modal-footer { + border-top-color: var(--yasgui-border-color); + } + + input, + textarea, + select { + background-color: var(--yasgui-bg-secondary); + color: var(--yasgui-text-primary); + border-color: var(--yasgui-input-border); + + &:focus { + border-color: var(--yasgui-input-focus); + background-color: var(--yasgui-bg-primary); + } + } +} From 9e059d2be34ffcbca722c1660e86264bff913990 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:04:51 +0000 Subject: [PATCH 3/9] Add theme documentation and export Theme type Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com> --- README.md | 11 +++ THEME_GUIDE.md | 162 +++++++++++++++++++++++++++++++++++ packages/yasgui/src/index.ts | 1 + 3 files changed, 174 insertions(+) create mode 100644 THEME_GUIDE.md diff --git a/README.md b/README.md index ab008baa..95ee307a 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,17 @@ Go to https://yasgui.matdata.eu/ and use it freely in production. Or fork this r ## Features +### Themes + +Yasgui supports both **light** and **dark** themes with instant switching: + +- **Theme Toggle Button**: Quickly switch between light and dark modes using the button in the tab bar +- **Persistent Preference**: Your theme choice is automatically saved +- **System Detection**: Automatically matches your system's dark/light mode preference +- **Full Coverage**: Consistent theming across all components (editor, results, modals) + +See the [Theme Guide](./THEME_GUIDE.md) for detailed configuration options and usage examples. + ### Keyboard Shortcuts #### Query Editor (Yasqe) diff --git a/THEME_GUIDE.md b/THEME_GUIDE.md new file mode 100644 index 00000000..dd489ddc --- /dev/null +++ b/THEME_GUIDE.md @@ -0,0 +1,162 @@ +# Yasgui Theme Guide + +Yasgui now supports both light and dark themes, allowing users to customize the appearance of the SPARQL IDE according to their preferences. + +## Features + +- **Light Theme**: Default bright theme suitable for well-lit environments +- **Dark Theme**: Easy on the eyes dark theme with syntax highlighting +- **Theme Toggle**: Quick switching between themes via UI button +- **Persistence**: User's theme preference is saved in localStorage +- **System Preference**: Automatically detects and applies system theme preference +- **Programmatic Control**: Set theme via JavaScript API + +## Usage + +### Theme Toggle Button + +By default, a theme toggle button appears in the tab bar (top-right area). Users can click this button to instantly switch between light and dark themes. + +- **Light Mode**: Shows a sun icon (☀️) - clicking switches to dark mode +- **Dark Mode**: Shows a moon icon (🌙) - clicking switches to light mode + +### Configuration Options + +You can configure the theme when initializing Yasgui: + +```javascript +import Yasgui from '@matdata/yasgui'; + +// Initialize with dark theme +const yasgui = new Yasgui(document.getElementById('yasgui'), { + theme: 'dark' +}); + +// Initialize with light theme (default) +const yasgui = new Yasgui(document.getElementById('yasgui'), { + theme: 'light' +}); + +// Hide the theme toggle button +const yasgui = new Yasgui(document.getElementById('yasgui'), { + showThemeToggle: false +}); +``` + +### Programmatic Theme Control + +You can control the theme programmatically using the Yasgui API: + +```javascript +// Get current theme +const currentTheme = yasgui.getTheme(); // Returns 'light' or 'dark' + +// Set theme +yasgui.setTheme('dark'); // Switch to dark theme +yasgui.setTheme('light'); // Switch to light theme + +// Toggle theme +const newTheme = yasgui.toggleTheme(); // Returns the new theme +``` + +## Theme Details + +### Light Theme +- Clean, bright interface with high contrast +- Default CodeMirror syntax highlighting +- White backgrounds with dark text +- Blue accent color (#337ab7) + +### Dark Theme +- Dark backgrounds with light text +- Material Palenight CodeMirror theme for syntax highlighting +- Reduced eye strain in low-light environments +- Cyan accent color (#4fc3f7) + +## Technical Details + +### CSS Custom Properties + +Themes are implemented using CSS custom properties (variables). The following variables are available: + +```css +--yasgui-bg-primary /* Primary background color */ +--yasgui-bg-secondary /* Secondary background (hover states, etc.) */ +--yasgui-bg-tertiary /* Tertiary background */ +--yasgui-text-primary /* Primary text color */ +--yasgui-text-secondary /* Secondary text color */ +--yasgui-text-muted /* Muted text color */ +--yasgui-border-color /* Primary border color */ +--yasgui-link-color /* Link color */ +--yasgui-accent-color /* Accent color for highlights */ +--yasgui-error-color /* Error message color */ +/* ... and more */ +``` + +### Theme Persistence + +The selected theme is automatically saved to localStorage under the key `yasgui_theme`. This means: + +- Theme preference persists across page reloads +- Each user's preference is independent +- No server-side configuration needed + +### System Theme Detection + +If no theme is explicitly set and no saved preference exists, Yasgui will: +1. Check the system's color scheme preference (`prefers-color-scheme` media query) +2. Apply dark theme if system prefers dark mode +3. Apply light theme otherwise + +The system theme listener will automatically update the theme if the user changes their system preference (and they haven't manually selected a theme in Yasgui). + +## TypeScript + +Theme types are exported for TypeScript users: + +```typescript +import Yasgui, { Theme } from '@matdata/yasgui'; + +const theme: Theme = 'dark'; // Type-safe: only 'light' or 'dark' allowed +yasgui.setTheme(theme); +``` + +## Browser Compatibility + +Themes work in all modern browsers that support: +- CSS Custom Properties +- localStorage +- Media Queries (for system theme detection) + +This includes all recent versions of Chrome, Firefox, Safari, and Edge. + +## Customization + +If you want to customize the theme colors, you can override the CSS custom properties in your own stylesheet: + +```css +/* Custom dark theme colors */ +[data-theme="dark"] { + --yasgui-bg-primary: #0d1117; + --yasgui-accent-color: #58a6ff; + /* Override other variables as needed */ +} +``` + +## Troubleshooting + +**Theme doesn't persist after page reload** + +Ensure localStorage is enabled in the browser. Some browsers in private/incognito mode may not persist localStorage data. + +**Theme toggle button is not visible** + +Check that `showThemeToggle` is not set to `false` in the configuration. + +**CodeMirror theme doesn't match** + +The CodeMirror theme is automatically synchronized with the Yasgui theme. If you see a mismatch, try manually refreshing: + +```javascript +yasgui.themeManager.refresh(); +``` diff --git a/packages/yasgui/src/index.ts b/packages/yasgui/src/index.ts index 03e1975b..1cd52ff2 100644 --- a/packages/yasgui/src/index.ts +++ b/packages/yasgui/src/index.ts @@ -401,4 +401,5 @@ export function getRandomId() { return Math.random().toString(36).substring(7); } +export type { Theme } from "./ThemeManager"; export default Yasgui; From 2db2b69bd1b43eb2ee86988072df9955edde2342 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:07:27 +0000 Subject: [PATCH 4/9] Address code review feedback - improve theme management Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com> --- packages/yasgui/src/TabElements.ts | 23 ++++++++++++----------- packages/yasgui/src/ThemeManager.ts | 5 +++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/yasgui/src/TabElements.ts b/packages/yasgui/src/TabElements.ts index f3d51fe1..379005e2 100644 --- a/packages/yasgui/src/TabElements.ts +++ b/packages/yasgui/src/TabElements.ts @@ -3,6 +3,15 @@ import TabContextMenu from "./TabContextMenu"; import { hasClass, addClass, removeClass } from "@matdata/yasgui-utils"; import sortablejs from "sortablejs"; import "./TabElements.scss"; + +// Theme toggle icons +const MOON_ICON = ` + +`; + +const SUN_ICON = ` + +`; export interface TabList {} export class TabListEl { private tabList: TabList; @@ -337,17 +346,9 @@ export class TabList { } private getThemeToggleIcon(): string { const currentTheme = this.yasgui.getTheme(); - if (currentTheme === "dark") { - // Moon icon for dark mode (clicking will switch to light) - return ` - - `; - } else { - // Sun icon for light mode (clicking will switch to dark) - return ` - - `; - } + // In dark mode, show moon icon (clicking will switch to light) + // In light mode, show sun icon (clicking will switch to dark) + return currentTheme === "dark" ? MOON_ICON : SUN_ICON; } public destroy() { for (const tabId in this._tabs) { diff --git a/packages/yasgui/src/ThemeManager.ts b/packages/yasgui/src/ThemeManager.ts index 73ba1c22..84712e7b 100644 --- a/packages/yasgui/src/ThemeManager.ts +++ b/packages/yasgui/src/ThemeManager.ts @@ -63,8 +63,8 @@ export class ThemeManager { private updateCodeMirrorTheme(theme: Theme): void { const cmTheme = theme === "dark" ? "material-palenight" : "default"; - // Find all CodeMirror instances within the root element and in document - const cmElements = document.querySelectorAll(".CodeMirror"); + // Find all CodeMirror instances within the root element + const cmElements = this.rootElement.querySelectorAll(".CodeMirror"); cmElements.forEach((element) => { const cm = (element as any).CodeMirror; if (cm && cm.setOption) { @@ -104,6 +104,7 @@ export class ThemeManager { // Only update if user hasn't manually set a theme if (!this.storage.get(ThemeManager.STORAGE_KEY)) { const newTheme = e.matches ? "dark" : "light"; + this.currentTheme = newTheme; this.applyTheme(newTheme); } }); From 958384921c2e757cb0a2007b4bfacbb2c761e396 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:55:23 +0000 Subject: [PATCH 5/9] Changes before error encountered Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com> --- packages/yasgui/src/themes.scss | 164 ++++++++++++++++++++ packages/yasr/src/plugins/response/index.ts | 6 + 2 files changed, 170 insertions(+) diff --git a/packages/yasgui/src/themes.scss b/packages/yasgui/src/themes.scss index 22d9e298..af7b502b 100644 --- a/packages/yasgui/src/themes.scss +++ b/packages/yasgui/src/themes.scss @@ -158,6 +158,62 @@ height: 20px; } } + + // Endpoint textbox theming + .autocomplete { + background-color: var(--yasgui-bg-primary) !important; + color: var(--yasgui-text-primary) !important; + border-color: var(--yasgui-border-color) !important; + + &:hover { + border-color: var(--yasgui-border-color-light) !important; + } + + &:focus { + border-color: var(--yasgui-accent-color) !important; + } + } + + .autocompleteList { + background: var(--yasgui-bg-primary) !important; + border-color: var(--yasgui-border-color) !important; + + .autoComplete_result { + color: var(--yasgui-text-primary); + + b { + color: var(--yasgui-accent-color) !important; + } + + &.autoComplete_selected, + &:hover { + background: var(--yasgui-bg-secondary) !important; + } + + .removeItem { + color: var(--yasgui-text-primary) !important; + text-shadow: none !important; + + &:hover { + color: var(--yasgui-accent-color) !important; + } + } + } + + .noResults { + color: var(--yasgui-text-muted); + } + } + + .clearEndpointBtn { + background-color: var(--yasgui-bg-secondary) !important; + border-color: var(--yasgui-border-color) !important; + color: var(--yasgui-button-text) !important; + + &:hover { + background-color: var(--yasgui-bg-tertiary) !important; + } + } } // Yasqe (editor) theming @@ -186,6 +242,65 @@ .parseErrorIcon svg g { fill: var(--yasgui-error-color); } + + // Yasqe buttons theming (share, query, fullscreen) + .yasqe_buttons { + svg { + fill: var(--yasgui-button-text) !important; + } + + .yasqe_share { + &:hover svg { + fill: var(--yasgui-button-hover) !important; + } + } + + .yasqe_fullscreenButton { + &:hover svg { + fill: var(--yasgui-accent-color) !important; + } + } + + .yasqe_sharePopup { + background-color: var(--yasgui-bg-primary) !important; + border-color: var(--yasgui-border-color) !important; + + input, + textarea { + background-color: var(--yasgui-bg-secondary); + color: var(--yasgui-text-primary); + border-color: var(--yasgui-border-color); + } + } + } + + .yasqe_btn { + background-color: var(--yasgui-bg-primary) !important; + color: var(--yasgui-text-primary) !important; + border-color: var(--yasgui-border-color) !important; + + &:hover { + background-color: var(--yasgui-bg-secondary) !important; + border-color: var(--yasgui-border-color-light) !important; + } + + &:focus, + &.selected { + background-color: var(--yasgui-accent-color) !important; + border-color: var(--yasgui-accent-color) !important; + color: var(--yasgui-bg-primary) !important; + } + + &.btn_icon:focus { + background-color: var(--yasgui-bg-primary) !important; + color: var(--yasgui-text-primary) !important; + border-color: var(--yasgui-border-color) !important; + } + } + + &.fullscreen { + background: var(--yasgui-bg-primary) !important; + } } // Yasr (results) theming @@ -292,6 +407,55 @@ } // Response plugin theming + .yasr_results { + .CodeMirror { + background-color: var(--yasgui-bg-primary) !important; + color: var(--yasgui-text-primary) !important; + border-color: var(--yasgui-border-color) !important; + + &.overflow::before { + background: linear-gradient( + transparent 0%, + var(--yasgui-bg-primary) 75%, + var(--yasgui-bg-primary) 100% + ) !important; + } + } + + .overlay_content { + background: var(--yasgui-bg-primary) !important; + background: linear-gradient( + to right, + transparent, + var(--yasgui-bg-primary) 5%, + var(--yasgui-bg-primary) 95%, + transparent 100% + ) !important; + } + + .yasr_btn.overlay_btn { + background: var(--yasgui-bg-primary) !important; + color: var(--yasgui-accent-color) !important; + border-color: var(--yasgui-accent-color) !important; + + svg { + fill: var(--yasgui-accent-color) !important; + color: var(--yasgui-accent-color) !important; + } + + &:hover { + color: var(--yasgui-link-hover) !important; + border-color: var(--yasgui-accent-color) !important; + fill: var(--yasgui-link-hover) !important; + + svg { + color: var(--yasgui-link-hover) !important; + fill: var(--yasgui-link-hover) !important; + } + } + } + } + .cm-s-default { background-color: var(--yasgui-bg-primary); color: var(--yasgui-text-primary); diff --git a/packages/yasr/src/plugins/response/index.ts b/packages/yasr/src/plugins/response/index.ts index 93286c14..3bde726a 100644 --- a/packages/yasr/src/plugins/response/index.ts +++ b/packages/yasr/src/plugins/response/index.ts @@ -15,6 +15,7 @@ import "codemirror/mode/xml/xml.js"; import "codemirror/mode/javascript/javascript.js"; import "codemirror/lib/codemirror.css"; +import "codemirror/theme/material-palenight.css"; import { drawSvgStringAsElement, addClass, removeClass, drawFontAwesomeIconAsSvg } from "@matdata/yasgui-utils"; import * as faAlignIcon from "@fortawesome/free-solid-svg-icons/faAlignLeft"; import { DeepReadonly } from "ts-essentials"; @@ -78,6 +79,10 @@ export default class Response implements Plugin { value = lines.slice(0, config.maxLines).join("\n"); } + // Detect current theme from document + const isDarkTheme = document.documentElement.getAttribute("data-theme") === "dark"; + const cmTheme = isDarkTheme ? "material-palenight" : "default"; + const codemirrorOpts: Partial = { readOnly: true, lineNumbers: true, @@ -86,6 +91,7 @@ export default class Response implements Plugin { gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], value: value, extraKeys: { Tab: false }, + theme: cmTheme, }; const mode = this.yasr.results?.getType(); if (mode === "json") { From a92b25ee6c8a13ae024e327f81bbbc230afd9291 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:47:03 +0000 Subject: [PATCH 6/9] Fix theme toggle position and tab label styling - Move theme toggle button from tab bar to control bar (between Settings and PREFIX button) - Fix "Query" tab label color to use theme variables - Add theming for notification hints and CodeMirror autocomplete hints - Update TabSettingsModal to handle theme toggle button creation Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com> --- packages/yasgui/src/TabElements.ts | 20 --------------- packages/yasgui/src/TabSettingsModal.scss | 25 ++++++++++++++++++ packages/yasgui/src/TabSettingsModal.ts | 31 +++++++++++++++++++++++ packages/yasgui/src/themes.scss | 31 +++++++++++++++++++++-- 4 files changed, 85 insertions(+), 22 deletions(-) diff --git a/packages/yasgui/src/TabElements.ts b/packages/yasgui/src/TabElements.ts index 379005e2..70e163db 100644 --- a/packages/yasgui/src/TabElements.ts +++ b/packages/yasgui/src/TabElements.ts @@ -257,20 +257,6 @@ export class TabList { }, }); - // Add theme toggle button if enabled - if (this.yasgui.config.showThemeToggle) { - const themeToggleEl = document.createElement("button"); - themeToggleEl.className = "themeToggle"; - themeToggleEl.title = "Toggle theme"; - themeToggleEl.setAttribute("aria-label", "Toggle between light and dark theme"); - themeToggleEl.innerHTML = this.getThemeToggleIcon(); - themeToggleEl.addEventListener("click", () => { - this.yasgui.toggleTheme(); - themeToggleEl.innerHTML = this.getThemeToggleIcon(); - }); - this._tabsListEl.appendChild(themeToggleEl); - } - this.addTabEl = document.createElement("div"); this.addTabEl.setAttribute("role", "presentation"); @@ -344,12 +330,6 @@ export class TabList { this._tabsListEl?.insertBefore(this._tabs[tabId].draw(tabConf.name), this.addTabEl || null); } } - private getThemeToggleIcon(): string { - const currentTheme = this.yasgui.getTheme(); - // In dark mode, show moon icon (clicking will switch to light) - // In light mode, show sun icon (clicking will switch to dark) - return currentTheme === "dark" ? MOON_ICON : SUN_ICON; - } public destroy() { for (const tabId in this._tabs) { const tab = this._tabs[tabId]; diff --git a/packages/yasgui/src/TabSettingsModal.scss b/packages/yasgui/src/TabSettingsModal.scss index 25b3af92..b3435d43 100644 --- a/packages/yasgui/src/TabSettingsModal.scss +++ b/packages/yasgui/src/TabSettingsModal.scss @@ -1,3 +1,28 @@ +.themeToggle { + cursor: pointer; + background: transparent; + color: inherit; + border: none; + padding: 6px 8px; + margin-left: 10px; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 20px; + height: 20px; + } + + &:hover { + opacity: 0.7; + } + + &:active { + opacity: 0.5; + } +} + .tabPrefixButton { cursor: pointer; background: transparent; diff --git a/packages/yasgui/src/TabSettingsModal.ts b/packages/yasgui/src/TabSettingsModal.ts index 1b7bbe9d..b3b45174 100644 --- a/packages/yasgui/src/TabSettingsModal.ts +++ b/packages/yasgui/src/TabSettingsModal.ts @@ -2,6 +2,15 @@ import { addClass, removeClass, drawSvgStringAsElement } from "@matdata/yasgui-u import "./TabSettingsModal.scss"; import Tab from "./Tab"; +// Theme toggle icons +const MOON_ICON = ` + +`; + +const SUN_ICON = ` + +`; + const AcceptOptionsMap: { key: string; value: string }[] = [ { key: "JSON", value: "application/sparql-results+json" }, { key: "XML", value: "application/sparql-results+xml" }, @@ -24,6 +33,7 @@ export default class TabSettingsModal { private modalOverlay!: HTMLElement; private modalContent!: HTMLElement; private settingsButton!: HTMLButtonElement; + private themeToggleButton!: HTMLButtonElement; private prefixButton!: HTMLButtonElement; private prefixTextarea!: HTMLTextAreaElement; private autoCaptureCheckbox!: HTMLInputElement; @@ -50,6 +60,20 @@ export default class TabSettingsModal { controlBarEl.appendChild(this.settingsButton); this.settingsButton.onclick = () => this.open(); + // Theme toggle button (if enabled) + if (this.tab.yasgui.config.showThemeToggle) { + this.themeToggleButton = document.createElement("button"); + this.themeToggleButton.className = "themeToggle"; + this.themeToggleButton.title = "Toggle theme"; + this.themeToggleButton.setAttribute("aria-label", "Toggle between light and dark theme"); + this.themeToggleButton.innerHTML = this.getThemeToggleIcon(); + this.themeToggleButton.addEventListener("click", () => { + this.tab.yasgui.toggleTheme(); + this.themeToggleButton.innerHTML = this.getThemeToggleIcon(); + }); + controlBarEl.appendChild(this.themeToggleButton); + } + // Prefix button this.prefixButton = document.createElement("button"); this.prefixButton.setAttribute("aria-label", "Insert Prefixes"); @@ -416,6 +440,13 @@ export default class TabSettingsModal { this.tab.yasgui.persistentConfig.setPrefixes(deduplicated); } + private getThemeToggleIcon(): string { + const currentTheme = this.tab.yasgui.getTheme(); + // In dark mode, show moon icon (clicking will switch to light) + // In light mode, show sun icon (clicking will switch to dark) + return currentTheme === "dark" ? MOON_ICON : SUN_ICON; + } + public destroy() { if (this.modalOverlay && this.modalOverlay.parentNode) { this.modalOverlay.parentNode.removeChild(this.modalOverlay); diff --git a/packages/yasgui/src/themes.scss b/packages/yasgui/src/themes.scss index af7b502b..aae4e3be 100644 --- a/packages/yasgui/src/themes.scss +++ b/packages/yasgui/src/themes.scss @@ -100,13 +100,25 @@ color: var(--yasgui-text-primary); border-color: var(--yasgui-tab-border); + a { + color: var(--yasgui-text-primary); + } + &.active { background: var(--yasgui-tab-bg-active); border-bottom-color: var(--yasgui-tab-bg-active); + + a { + color: var(--yasgui-text-primary); + } } &:hover:not(.active) { background: var(--yasgui-bg-secondary); + + a { + color: var(--yasgui-text-primary); + } } } @@ -226,8 +238,8 @@ } .notification { - color: var(--yasgui-notification-text); - background-color: var(--yasgui-notification-bg); + color: var(--yasgui-notification-text) !important; + background-color: var(--yasgui-notification-bg) !important; } .yasqe_tooltip { @@ -303,6 +315,21 @@ } } +// CodeMirror hints theming +.CodeMirror-hints { + background: var(--yasgui-bg-primary) !important; + border-color: var(--yasgui-border-color) !important; +} + +.CodeMirror-hint { + color: var(--yasgui-text-primary) !important; +} + +li.CodeMirror-hint-active { + background: var(--yasgui-accent-color) !important; + color: var(--yasgui-bg-primary) !important; +} + // Yasr (results) theming .yasr { background-color: var(--yasgui-bg-primary); From 433228f83f71df6f939f7c86be417b32406aaebc Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sun, 7 Dec 2025 22:10:14 +0100 Subject: [PATCH 7/9] chore: also make the setting modal window and several other elements change theme --- packages/yasgui/src/TabElements.scss | 12 +++--- packages/yasgui/src/TabSettingsModal.scss | 47 +++++++++++++---------- packages/yasgui/src/TabSettingsModal.ts | 25 ++++++------ packages/yasgui/src/tab.scss | 16 +++++--- packages/yasqe/src/scss/yasqe.scss | 4 +- packages/yasr/src/main.scss | 14 +++---- 6 files changed, 63 insertions(+), 55 deletions(-) diff --git a/packages/yasgui/src/TabElements.scss b/packages/yasgui/src/TabElements.scss index b2e9f496..81d767c7 100644 --- a/packages/yasgui/src/TabElements.scss +++ b/packages/yasgui/src/TabElements.scss @@ -107,7 +107,7 @@ $minTabHeight: 35px; } &.active a { border-bottom-color: $activeColor; - color: #555; + color: var(--yasgui-text-primary, #555); } input { display: none; @@ -127,7 +127,7 @@ $minTabHeight: 35px; } a { font-weight: 600; - color: color.adjust(#555, $lightness: 20%); + color: var(--yasgui-text-secondary, color.adjust(#555, $lightness: 20%)); font-size: 15px; line-height: 1.5rem; font-weight: 500; @@ -137,17 +137,17 @@ $minTabHeight: 35px; overflow: hidden; &:hover { border-bottom-color: $hoverColor; - color: #555; + color: var(--yasgui-text-primary, #555); } &:focus { border-bottom-color: #faa857; - color: #555; + color: var(--yasgui-text-primary, #555); } .closeTab { - color: #000; + color: var(--yasgui-text-primary, #000); margin-left: 7px; font-size: 15px; - text-shadow: 0 1px 0 #fff; + text-shadow: 0 1px 0 var(--yasgui-bg-primary, #fff); opacity: 0.2; font-weight: 700; padding: 2px; diff --git a/packages/yasgui/src/TabSettingsModal.scss b/packages/yasgui/src/TabSettingsModal.scss index b3435d43..a03c2b81 100644 --- a/packages/yasgui/src/TabSettingsModal.scss +++ b/packages/yasgui/src/TabSettingsModal.scss @@ -60,7 +60,7 @@ } .tabSettingsModal { - background: white; + background: var(--yasgui-bg-primary, white); border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); max-width: 800px; @@ -76,12 +76,13 @@ justify-content: space-between; align-items: center; padding: 20px; - border-bottom: 1px solid #e0e0e0; + border-bottom: 1px solid var(--yasgui-border-color, #e0e0e0); h2 { margin: 0; font-size: 20px; font-weight: 600; + color: var(--yasgui-text-primary, #000); } .closeButton { @@ -90,13 +91,13 @@ font-size: 32px; line-height: 1; cursor: pointer; - color: #666; + color: var(--yasgui-text-secondary, #666); padding: 0; width: 32px; height: 32px; &:hover { - color: #333; + color: var(--yasgui-text-primary, #333); } } } @@ -111,7 +112,7 @@ display: flex; gap: 10px; margin-bottom: 20px; - border-bottom: 2px solid #e0e0e0; + border-bottom: 2px solid var(--yasgui-border-color, #e0e0e0); } .modalTabButton { @@ -121,18 +122,18 @@ font-size: 14px; font-weight: 600; cursor: pointer; - color: #666; + color: var(--yasgui-text-secondary, #666); border-bottom: 3px solid transparent; margin-bottom: -2px; transition: all 0.2s; &:hover { - color: #337ab7; + color: var(--yasgui-accent-color, #337ab7); } &.active { - color: #337ab7; - border-bottom-color: #337ab7; + color: var(--yasgui-accent-color, #337ab7); + border-bottom-color: var(--yasgui-accent-color, #337ab7); } } @@ -153,11 +154,12 @@ margin-bottom: 8px; font-size: 14px; display: block; + color: var(--yasgui-text-primary, #000); } .settingsHelp { font-size: 12px; - color: #666; + color: var(--yasgui-text-secondary, #666); margin-bottom: 10px; font-style: italic; } @@ -165,13 +167,15 @@ .settingsSelect { width: 100%; padding: 8px; - border: 1px solid #ccc; + border: 1px solid var(--yasgui-input-border, #ccc); border-radius: 4px; font-size: 14px; + background-color: var(--yasgui-bg-secondary, white); + color: var(--yasgui-text-primary, #000); &:focus { outline: none; - border-color: #337ab7; + border-color: var(--yasgui-input-focus, #337ab7); box-shadow: 0 0 0 2px rgba(51, 122, 183, 0.1); } } @@ -179,17 +183,19 @@ .prefixTextarea { width: 100%; padding: 10px; - border: 1px solid #ccc; + border: 1px solid var(--yasgui-input-border, #ccc); border-radius: 4px; font-family: monospace; font-size: 13px; resize: vertical; box-sizing: border-box; min-height: 200px; + background-color: var(--yasgui-bg-secondary, white); + color: var(--yasgui-text-primary, #000); &:focus { outline: none; - border-color: #337ab7; + border-color: var(--yasgui-input-focus, #337ab7); box-shadow: 0 0 0 2px rgba(51, 122, 183, 0.1); } } @@ -211,12 +217,13 @@ font-size: 14px; user-select: none; margin: 0; + color: var(--yasgui-text-primary, #000); } } .modalFooter { padding: 15px 20px; - border-top: 1px solid #e0e0e0; + border-top: 1px solid var(--yasgui-border-color, #e0e0e0); display: flex; justify-content: flex-end; gap: 10px; @@ -233,19 +240,19 @@ } .primaryButton { - background: #337ab7; + background: var(--yasgui-accent-color, #337ab7); color: white; &:hover { - background: #286090; + background: var(--yasgui-link-hover, #286090); } } .secondaryButton { - background: #e0e0e0; - color: #333; + background: var(--yasgui-bg-tertiary, #e0e0e0); + color: var(--yasgui-text-primary, #333); &:hover { - background: #d0d0d0; + background: var(--yasgui-border-color, #d0d0d0); } } diff --git a/packages/yasgui/src/TabSettingsModal.ts b/packages/yasgui/src/TabSettingsModal.ts index b3b45174..a7e7529e 100644 --- a/packages/yasgui/src/TabSettingsModal.ts +++ b/packages/yasgui/src/TabSettingsModal.ts @@ -1,4 +1,4 @@ -import { addClass, removeClass, drawSvgStringAsElement } from "@matdata/yasgui-utils"; +import { addClass, removeClass } from "@matdata/yasgui-utils"; import "./TabSettingsModal.scss"; import Tab from "./Tab"; @@ -11,6 +11,10 @@ const SUN_ICON = ` `; +const SETTINGS_ICON = ` + +`; + const AcceptOptionsMap: { key: string; value: string }[] = [ { key: "JSON", value: "application/sparql-results+json" }, { key: "XML", value: "application/sparql-results+xml" }, @@ -46,26 +50,19 @@ export default class TabSettingsModal { private init(controlBarEl: HTMLElement) { // Settings button this.settingsButton = document.createElement("button"); + addClass(this.settingsButton, "tabContextButton"); this.settingsButton.setAttribute("aria-label", "Settings"); this.settingsButton.title = "Settings"; - this.settingsButton.appendChild( - drawSvgStringAsElement( - ` - Settings - - `, - ), - ); - addClass(this.settingsButton, "tabContextButton"); - controlBarEl.appendChild(this.settingsButton); + this.settingsButton.innerHTML = SETTINGS_ICON; this.settingsButton.onclick = () => this.open(); + controlBarEl.appendChild(this.settingsButton); // Theme toggle button (if enabled) if (this.tab.yasgui.config.showThemeToggle) { this.themeToggleButton = document.createElement("button"); - this.themeToggleButton.className = "themeToggle"; - this.themeToggleButton.title = "Toggle theme"; + addClass(this.themeToggleButton, "themeToggle"); this.themeToggleButton.setAttribute("aria-label", "Toggle between light and dark theme"); + this.themeToggleButton.title = "Toggle theme"; this.themeToggleButton.innerHTML = this.getThemeToggleIcon(); this.themeToggleButton.addEventListener("click", () => { this.tab.yasgui.toggleTheme(); @@ -101,7 +98,7 @@ export default class TabSettingsModal { const header = document.createElement("div"); addClass(header, "modalHeader"); const title = document.createElement("h2"); - title.textContent = "Tab Settings"; + title.textContent = "Settings"; header.appendChild(title); const closeBtn = document.createElement("button"); diff --git a/packages/yasgui/src/tab.scss b/packages/yasgui/src/tab.scss index 3575346f..8a2465a2 100644 --- a/packages/yasgui/src/tab.scss +++ b/packages/yasgui/src/tab.scss @@ -32,9 +32,13 @@ background: none; align-self: center; padding-left: 10px; + padding-right: 10px; cursor: pointer; - color: #505050; - fill: #505050; + color: var(--yasgui-button-text, #505050); + fill: var(--yasgui-button-text, #505050); + display: flex; + align-items: center; + justify-content: center; .svgImg { width: 15px; @@ -43,13 +47,13 @@ } // IE11 Needs this specified otherwise it will not resize the svg svg { - max-width: 15px; - max-height: 15px; + width: 20px; + height: 20px; } &:hover { - color: black; - fill: black; + color: var(--yasgui-button-hover, black); + fill: var(--yasgui-button-hover, black); } } diff --git a/packages/yasqe/src/scss/yasqe.scss b/packages/yasqe/src/scss/yasqe.scss index 5e67f0c2..fa3f891b 100644 --- a/packages/yasqe/src/scss/yasqe.scss +++ b/packages/yasqe/src/scss/yasqe.scss @@ -25,8 +25,8 @@ z-index: 4; padding: 0 5px; max-height: 0px; - color: #999; - background-color: #eee; + color: var(--yasgui-notification-text, #999); + background-color: var(--yasgui-notification-bg, #eee); font-size: 90%; text-align: center; transition: max-height 0.2s ease-in; diff --git a/packages/yasr/src/main.scss b/packages/yasr/src/main.scss index 5af682c5..6298cf5a 100644 --- a/packages/yasr/src/main.scss +++ b/packages/yasr/src/main.scss @@ -56,7 +56,7 @@ // redeclared here to target yasr_btns in btnGroup border-bottom: 2px solid transparent; &.selected { - border-bottom: 2px solid #337ab7; + border-bottom: 2px solid var(--yasgui-accent-color, #337ab7); } padding-left: 6px; padding-right: 6px; @@ -123,8 +123,8 @@ } } .yasr_btn { - color: #505050; - fill: #505050; + color: var(--yasgui-button-text, #505050); + fill: var(--yasgui-button-text, #505050); display: flex; align-items: center; justify-content: center; @@ -145,8 +145,8 @@ box-shadow: none; } &:not(.disabled):hover { - fill: black; - color: black; + fill: var(--yasgui-button-hover, black); + color: var(--yasgui-button-hover, black); } } @@ -191,8 +191,8 @@ } &:focus, &.selected { - color: #337ab7; - fill: #337ab7; + color: var(--yasgui-accent-color, #337ab7); + fill: var(--yasgui-accent-color, #337ab7); } } From 419853cd3e346b5073cee56b01dc83ee00b5a2bf Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sun, 7 Dec 2025 22:42:12 +0100 Subject: [PATCH 8/9] chore: changed line-ending --- .dockerignore | 8 +- .github/workflows/deploy-pages.yml | 116 ++++++------ .github/workflows/publish.yml | 118 ++++++------ .github/workflows/push-docker.yml | 98 +++++----- README.md | 280 ++++++++++++++--------------- docker/Dockerfile | 68 +++---- docker/src/manifest.json | 30 ++-- docker/src/robots.txt | 6 +- 8 files changed, 362 insertions(+), 362 deletions(-) diff --git a/.dockerignore b/.dockerignore index 289e7d21..35379377 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,5 @@ -/build -/node_modules -/.git -/.vscode +/build +/node_modules +/.git +/.vscode /.github \ No newline at end of file diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 8274e0ec..6e23b087 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -1,58 +1,58 @@ -name: Deploy to GitHub Pages - -on: - push: - branches: - - main - pull_request: - branches: - - main - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - deploy: - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: "lts/*" - cache: npm - - - name: Disable AppArmor - run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - - - name: Install dependencies - run: npm ci - - - name: Build project - run: npm run build - env: - PUBLIC_PATH: /Yasgui/ - - - name: Setup Pages - uses: actions/configure-pages@v4 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./build - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 +name: Deploy to GitHub Pages + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: "lts/*" + cache: npm + + - name: Disable AppArmor + run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + env: + PUBLIC_PATH: /Yasgui/ + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./build + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9d56bc41..ad009063 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,60 +1,60 @@ -name: Publish to npm - -on: - release: - types: [published] - -permissions: - contents: read - id-token: write - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - registry-url: 'https://registry.npmjs.org' - - # Ensure npm 11.5.1 or later for trusted publishing - - run: npm install -g npm@latest - - - name: Install dependencies - run: npm ci - - - name: Build packages - run: npm run build - - - name: Update package versions from release tag - run: | - TAG_VERSION=${GITHUB_REF#refs/tags/} - TAG_VERSION=${TAG_VERSION#v} - echo "Updating all packages to version $TAG_VERSION" - - # Update each package version - for package_dir in packages/*/; do - if [ -f "${package_dir}package.json" ]; then - echo "Updating version in ${package_dir}package.json" - cd "$package_dir" - npm version $TAG_VERSION --no-git-tag-version --allow-same-version - cd ../.. - fi - done - - - name: Copy README to all packages - run: | - echo "Copying README.md to all packages" - for package_dir in packages/*/; do - if [ -f "${package_dir}package.json" ]; then - echo "Copying README.md to ${package_dir}" - cp README.md "${package_dir}README.md" - fi - done - - - name: Publish to npm using Changesets +name: Publish to npm + +on: + release: + types: [published] + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + # Ensure npm 11.5.1 or later for trusted publishing + - run: npm install -g npm@latest + + - name: Install dependencies + run: npm ci + + - name: Build packages + run: npm run build + + - name: Update package versions from release tag + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/} + TAG_VERSION=${TAG_VERSION#v} + echo "Updating all packages to version $TAG_VERSION" + + # Update each package version + for package_dir in packages/*/; do + if [ -f "${package_dir}package.json" ]; then + echo "Updating version in ${package_dir}package.json" + cd "$package_dir" + npm version $TAG_VERSION --no-git-tag-version --allow-same-version + cd ../.. + fi + done + + - name: Copy README to all packages + run: | + echo "Copying README.md to all packages" + for package_dir in packages/*/; do + if [ -f "${package_dir}package.json" ]; then + echo "Copying README.md to ${package_dir}" + cp README.md "${package_dir}README.md" + fi + done + + - name: Publish to npm using Changesets run: npx changeset publish --no-git-tag \ No newline at end of file diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index b3002341..e6642d58 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -1,50 +1,50 @@ -name: Build and Push Docker Image - -permissions: - contents: read - id-token: write - -on: - release: - types: [published] - -jobs: - build-and-push: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 - - - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 - with: - images: ${{ vars.DOCKERHUB_REPO }} - - - name: Build and push Docker image - uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 - with: - context: . - file: ./docker/Dockerfile - push: true - build-args: GITHUB_REF=${{ github.ref_name }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64,linux/arm64 - - - uses: shrink/actions-docker-extract@f97780187d59e88f06a98b166a7e700c0f400314 - id: extract - with: - image: ${{ vars.DOCKERHUB_REPO }} - path: /usr/share/nginx/html/. +name: Build and Push Docker Image + +permissions: + contents: read + id-token: write + +on: + release: + types: [published] + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 + + - name: Login to DockerHub + uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ vars.DOCKERHUB_REPO }} + + - name: Build and push Docker image + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 + with: + context: . + file: ./docker/Dockerfile + push: true + build-args: GITHUB_REF=${{ github.ref_name }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + + - uses: shrink/actions-docker-extract@f97780187d59e88f06a98b166a7e700c0f400314 + id: extract + with: + image: ${{ vars.DOCKERHUB_REPO }} + path: /usr/share/nginx/html/. destination: dist \ No newline at end of file diff --git a/README.md b/README.md index c7552fc8..bceabdf3 100644 --- a/README.md +++ b/README.md @@ -1,140 +1,140 @@ -# YASGUI - -YASGUI (Yet Another SPARQL GUI) is an advanced SPARQL client for querying and exploring RDF data. It provides a user-friendly interface for writing SPARQL queries, executing them against SPARQL endpoints, and visualizing the results in various formats. - -Go to https://yasgui.matdata.eu/ and use it freely in production. Or fork this repo and extend it yourself. Contributions are certainly welcome! - -## Table of Contents - -- [Useful Links](#useful-links) -- [Features](#features) - - [Keyboard Shortcuts](#keyboard-shortcuts) - - [Query Editor (Yasqe)](#query-editor-yasqe) - - [Fullscreen Mode](#fullscreen-mode) - - [URI Explorer](#uri-explorer) - - [Prefix Management](#prefix-management) - - [Result Visualization Plugins (Yasr)](#result-visualization-plugins-yasr) -- [Installation](#installation) - - [npm](#npm) - - [Yarn](#yarn) -- [Local Development](#local-development) -- [License](#license) - -## Useful Links - -- Production environment: https://yasgui.matdata.eu/ -- Dev environment: https://yasgui-dev.matdata.eu/ (GitHub Pages - automatically updated with every commit to main branch) -- User documentation: https://docs.triply.cc/yasgui/ -- Developer documentation: https://docs.triply.cc/yasgui-api/ -- Docker Hub: https://hub.docker.com/r/mathiasvda/yasgui - -## Features - -### Themes - -Yasgui supports both **light** and **dark** themes with instant switching: - -- **Theme Toggle Button**: Quickly switch between light and dark modes using the button in the tab bar -- **Persistent Preference**: Your theme choice is automatically saved -- **System Detection**: Automatically matches your system's dark/light mode preference -- **Full Coverage**: Consistent theming across all components (editor, results, modals) - -See the [Theme Guide](./THEME_GUIDE.md) for detailed configuration options and usage examples. - -### Keyboard Shortcuts - -#### Query Editor (Yasqe) -- **Ctrl+Enter** / **Cmd+Enter**: Execute the current query -- **Ctrl+Space**: Trigger autocomplete -- **Ctrl+S**: Save query to local storage -- **Shift+Ctrl+F**: Auto-format the query -- **Ctrl+/**: Comment/uncomment selected lines -- **Shift+Ctrl+D**: Duplicate the current line -- **Shift+Ctrl+K**: Delete the current line -- **Esc**: Remove focus from the editor - -#### Fullscreen Mode -- **F11**: Toggle fullscreen mode for the query editor (Yasqe) -- **F10**: Toggle fullscreen mode for the results viewer (Yasr) -- **Ctrl+Shift+F**: Switch between Yasqe and Yasr fullscreen modes - -#### URI Explorer -- **Ctrl+Click** on any URI in the query editor: Automatically executes a CONSTRUCT query to explore the clicked URI's connections (incoming and outgoing triples). The query runs in the background without modifying your current query in the editor. - -### Prefix Management -- **PREFIX Button**: Insert saved prefix declarations into your query (replaces existing PREFIX lines at the beginning) -- **Settings Modal**: Access via the settings button (⚙) to manage: - - **Saved Prefixes**: Define reusable PREFIX declarations - - **Auto-capture**: Automatically captures new prefixes from your queries (enabled by default) - - **Request Configuration**: Configure HTTP request method, accept headers, arguments, headers, and named/default graphs -- **Default Prefixes**: Automatically includes `rdf:` and `rdfs:` prefixes for new users -- **Prefix Autocomplete**: When typing a prefix declaration (e.g., `PREFIX foaf:`), the editor automatically queries [prefix.cc](https://prefix.cc) to suggest and auto-complete the full URI commonly associated with that prefix. This helps you quickly add standard prefixes without needing to remember their full URIs. - -### Result Visualization Plugins (Yasr) - -Yasgui includes several built-in plugins to visualize SPARQL query results: - -- **Table**: Interactive table view with sorting, filtering, pagination, and column resizing. Ideal for SELECT query results. -- **Boolean**: Displays boolean results (true/false) with visual indicators. Automatically used for ASK queries. -- **Response**: Raw response viewer with syntax highlighting and code folding. Shows the original response from the endpoint in JSON, XML, Turtle, or other formats. -- **Geo**: Geographic visualization plugin for displaying spatial data on interactive maps. Visualizes geospatial triples with coordinates. See [Yasgui Geo TG Plugin](https://github.com/Thib-G/yasgui-geo-tg) for more details. -- **Graph**: Visual graph representation of RDF data using nodes and edges. Ideal for CONSTRUCT/DESCRIBE query results. See [Yasgui Graph Plugin](https://github.com/Matdata-eu/yasgui-graph-plugin) for more details. -- **Error**: Displays error messages and diagnostics when queries fail, including CORS troubleshooting guidance. - -Plugins are automatically selected based on the query type and response format. You can manually switch between available plugins using the view selector in the results pane. - -## Installation - -Below are instructions on how to include Yasgui in your project. -If you only want to install Yasr or Yasqe, replace yasgui in the commands below. - -### npm - -```sh -npm i @matdata/yasgui -``` - -### Yarn - -```sh -yarn add @matdata/yasgui -``` - -### Docker - -Yasgui is also available as a Docker image on Docker Hub. Images are built and published automatically when a new release is created. - -```sh -docker pull mathiasvda/yasgui:latest -docker run -p 8080:8080 mathiasvda/yasgui:latest -``` - -You can customize the default SPARQL endpoint by setting the `YASGUI_DEFAULT_ENDPOINT` environment variable: - -```sh -docker run -p 8080:8080 -e YASGUI_DEFAULT_ENDPOINT=https://your-endpoint.com/sparql mathiasvda/yasgui:latest -``` - -The application will be available at `http://localhost:8080`. - -## Local Development - -#### Installing dependencies - -Run `npm install`. - -#### Running Yasgui locally - -To develop locally, run `npm run dev` - -Go to `http://localhost:5173/demo` in your browser to see Yasgui in action. - -#### Compiling Yasgui - -Run `npm run build`. It'll store the transpiled js/css files in the `build` directory. - -## License - -This is a fork from [Zazuko](https://github.com/zazuko/Yasgui) who forked it from [Triply](https://github.com/TriplyDB/Yasgui). - -This code is released under the MIT license. +# YASGUI + +YASGUI (Yet Another SPARQL GUI) is an advanced SPARQL client for querying and exploring RDF data. It provides a user-friendly interface for writing SPARQL queries, executing them against SPARQL endpoints, and visualizing the results in various formats. + +Go to https://yasgui.matdata.eu/ and use it freely in production. Or fork this repo and extend it yourself. Contributions are certainly welcome! + +## Table of Contents + +- [Useful Links](#useful-links) +- [Features](#features) + - [Keyboard Shortcuts](#keyboard-shortcuts) + - [Query Editor (Yasqe)](#query-editor-yasqe) + - [Fullscreen Mode](#fullscreen-mode) + - [URI Explorer](#uri-explorer) + - [Prefix Management](#prefix-management) + - [Result Visualization Plugins (Yasr)](#result-visualization-plugins-yasr) +- [Installation](#installation) + - [npm](#npm) + - [Yarn](#yarn) +- [Local Development](#local-development) +- [License](#license) + +## Useful Links + +- Production environment: https://yasgui.matdata.eu/ +- Dev environment: https://yasgui-dev.matdata.eu/ (GitHub Pages - automatically updated with every commit to main branch) +- User documentation: https://docs.triply.cc/yasgui/ +- Developer documentation: https://docs.triply.cc/yasgui-api/ +- Docker Hub: https://hub.docker.com/r/mathiasvda/yasgui + +## Features + +### Themes + +Yasgui supports both **light** and **dark** themes with instant switching: + +- **Theme Toggle Button**: Quickly switch between light and dark modes using the button in the tab bar +- **Persistent Preference**: Your theme choice is automatically saved +- **System Detection**: Automatically matches your system's dark/light mode preference +- **Full Coverage**: Consistent theming across all components (editor, results, modals) + +See the [Theme Guide](./THEME_GUIDE.md) for detailed configuration options and usage examples. + +### Keyboard Shortcuts + +#### Query Editor (Yasqe) +- **Ctrl+Enter** / **Cmd+Enter**: Execute the current query +- **Ctrl+Space**: Trigger autocomplete +- **Ctrl+S**: Save query to local storage +- **Shift+Ctrl+F**: Auto-format the query +- **Ctrl+/**: Comment/uncomment selected lines +- **Shift+Ctrl+D**: Duplicate the current line +- **Shift+Ctrl+K**: Delete the current line +- **Esc**: Remove focus from the editor + +#### Fullscreen Mode +- **F11**: Toggle fullscreen mode for the query editor (Yasqe) +- **F10**: Toggle fullscreen mode for the results viewer (Yasr) +- **Ctrl+Shift+F**: Switch between Yasqe and Yasr fullscreen modes + +#### URI Explorer +- **Ctrl+Click** on any URI in the query editor: Automatically executes a CONSTRUCT query to explore the clicked URI's connections (incoming and outgoing triples). The query runs in the background without modifying your current query in the editor. + +### Prefix Management +- **PREFIX Button**: Insert saved prefix declarations into your query (replaces existing PREFIX lines at the beginning) +- **Settings Modal**: Access via the settings button (⚙) to manage: + - **Saved Prefixes**: Define reusable PREFIX declarations + - **Auto-capture**: Automatically captures new prefixes from your queries (enabled by default) + - **Request Configuration**: Configure HTTP request method, accept headers, arguments, headers, and named/default graphs +- **Default Prefixes**: Automatically includes `rdf:` and `rdfs:` prefixes for new users +- **Prefix Autocomplete**: When typing a prefix declaration (e.g., `PREFIX foaf:`), the editor automatically queries [prefix.cc](https://prefix.cc) to suggest and auto-complete the full URI commonly associated with that prefix. This helps you quickly add standard prefixes without needing to remember their full URIs. + +### Result Visualization Plugins (Yasr) + +Yasgui includes several built-in plugins to visualize SPARQL query results: + +- **Table**: Interactive table view with sorting, filtering, pagination, and column resizing. Ideal for SELECT query results. +- **Boolean**: Displays boolean results (true/false) with visual indicators. Automatically used for ASK queries. +- **Response**: Raw response viewer with syntax highlighting and code folding. Shows the original response from the endpoint in JSON, XML, Turtle, or other formats. +- **Geo**: Geographic visualization plugin for displaying spatial data on interactive maps. Visualizes geospatial triples with coordinates. See [Yasgui Geo TG Plugin](https://github.com/Thib-G/yasgui-geo-tg) for more details. +- **Graph**: Visual graph representation of RDF data using nodes and edges. Ideal for CONSTRUCT/DESCRIBE query results. See [Yasgui Graph Plugin](https://github.com/Matdata-eu/yasgui-graph-plugin) for more details. +- **Error**: Displays error messages and diagnostics when queries fail, including CORS troubleshooting guidance. + +Plugins are automatically selected based on the query type and response format. You can manually switch between available plugins using the view selector in the results pane. + +## Installation + +Below are instructions on how to include Yasgui in your project. +If you only want to install Yasr or Yasqe, replace yasgui in the commands below. + +### npm + +```sh +npm i @matdata/yasgui +``` + +### Yarn + +```sh +yarn add @matdata/yasgui +``` + +### Docker + +Yasgui is also available as a Docker image on Docker Hub. Images are built and published automatically when a new release is created. + +```sh +docker pull mathiasvda/yasgui:latest +docker run -p 8080:8080 mathiasvda/yasgui:latest +``` + +You can customize the default SPARQL endpoint by setting the `YASGUI_DEFAULT_ENDPOINT` environment variable: + +```sh +docker run -p 8080:8080 -e YASGUI_DEFAULT_ENDPOINT=https://your-endpoint.com/sparql mathiasvda/yasgui:latest +``` + +The application will be available at `http://localhost:8080`. + +## Local Development + +#### Installing dependencies + +Run `npm install`. + +#### Running Yasgui locally + +To develop locally, run `npm run dev` + +Go to `http://localhost:5173/demo` in your browser to see Yasgui in action. + +#### Compiling Yasgui + +Run `npm run build`. It'll store the transpiled js/css files in the `build` directory. + +## License + +This is a fork from [Zazuko](https://github.com/zazuko/Yasgui) who forked it from [Triply](https://github.com/TriplyDB/Yasgui). + +This code is released under the MIT license. diff --git a/docker/Dockerfile b/docker/Dockerfile index 37d6eec4..30020096 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,35 +1,35 @@ -# Use the official Node.js image as the base image -FROM node:20 - -# Set the working directory -WORKDIR /app - -# Copy package.json and package-lock.json -COPY package*.json ./ - -# Install dependencies -RUN npm install - -# Copy the rest of the application code -COPY . . - -# Build the React app -RUN npm run build - -# Use the official Nginx image to serve the React app -FROM nginxinc/nginx-unprivileged:alpine - -# Copy only JS/CSS from the build output -COPY --from=0 /app/build/yasgui.min.js /usr/share/nginx/html/ -COPY --from=0 /app/build/yasgui.min.css /usr/share/nginx/html/ - -# Copy other static assets -COPY docker/src/ /usr/share/nginx/html/ - -USER root - -COPY docker/change-default-endpoint.sh /docker-entrypoint.d/ -RUN chmod +x /docker-entrypoint.d/change-default-endpoint.sh -RUN chown 1001:0 -R /usr/share/nginx/html/ - +# Use the official Node.js image as the base image +FROM node:20 + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the rest of the application code +COPY . . + +# Build the React app +RUN npm run build + +# Use the official Nginx image to serve the React app +FROM nginxinc/nginx-unprivileged:alpine + +# Copy only JS/CSS from the build output +COPY --from=0 /app/build/yasgui.min.js /usr/share/nginx/html/ +COPY --from=0 /app/build/yasgui.min.css /usr/share/nginx/html/ + +# Copy other static assets +COPY docker/src/ /usr/share/nginx/html/ + +USER root + +COPY docker/change-default-endpoint.sh /docker-entrypoint.d/ +RUN chmod +x /docker-entrypoint.d/change-default-endpoint.sh +RUN chown 1001:0 -R /usr/share/nginx/html/ + USER 1001 \ No newline at end of file diff --git a/docker/src/manifest.json b/docker/src/manifest.json index 9c89c9f9..07705a94 100644 --- a/docker/src/manifest.json +++ b/docker/src/manifest.json @@ -1,15 +1,15 @@ -{ - "short_name": "Yasgui", - "name": "Yet Another SPARQL GUI", - "icons": [ - { - "src": "favicon.png", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} +{ + "short_name": "Yasgui", + "name": "Yet Another SPARQL GUI", + "icons": [ + { + "src": "favicon.png", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/docker/src/robots.txt b/docker/src/robots.txt index e9e57dc4..9dd52a51 100644 --- a/docker/src/robots.txt +++ b/docker/src/robots.txt @@ -1,3 +1,3 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: From ecfd695e9068732679bfce05d40581269f87fcd5 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sun, 7 Dec 2025 22:46:27 +0100 Subject: [PATCH 9/9] feat: make the dev and docker pages change theme as well --- dev/yasgui.html | 153 +++++++++++++++++++++++++++--------------- docker/src/index.html | 123 +++++++++++++++++---------------- docker/src/main.css | 40 ++++++++++- docker/src/main.js | 23 +++++++ 4 files changed, 227 insertions(+), 112 deletions(-) diff --git a/dev/yasgui.html b/dev/yasgui.html index a7a9214d..5bc4283d 100644 --- a/dev/yasgui.html +++ b/dev/yasgui.html @@ -1,54 +1,99 @@ - - - - - - - Yasgui - - -
- -
- -
- - - - + + + + + + + Yasgui + + + + +
+ + + + diff --git a/docker/src/index.html b/docker/src/index.html index 8b188298..3cab6111 100644 --- a/docker/src/index.html +++ b/docker/src/index.html @@ -1,57 +1,68 @@ - - - - - - - - - - - - - - - - - - - - - - Yasgui - - - - - - -
- - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + Yasgui + + + + + + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/docker/src/main.css b/docker/src/main.css index f1facb7f..4622bb2c 100644 --- a/docker/src/main.css +++ b/docker/src/main.css @@ -7,6 +7,13 @@ body { "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + background-color: #ffffff; + transition: background-color 0.3s ease; +} + +/* Dark theme styles */ +[data-theme="dark"] body { + background-color: #1e1e1e; } /* YASGUI container */ @@ -28,7 +35,15 @@ body { align-items: center; justify-content: center; z-index: 1000; - transition: transform 0.3s ease-in-out; + transition: + transform 0.3s ease-in-out, + background-color 0.3s ease, + border-color 0.3s ease; +} + +[data-theme="dark"] #footer { + background-color: #2d2d30; + border-top: 1px solid #3e3e42; } .footer-left { @@ -43,7 +58,7 @@ body { } #logo { - height: 35px; + height: 20px; width: auto; transition: opacity 0.2s; } @@ -52,6 +67,15 @@ body { opacity: 0.8; } +.logo-primary { + fill: #2384eb; + transition: fill 0.3s ease; +} + +[data-theme="dark"] .logo-primary { + fill: #4fc3f7; +} + #footer a { color: #0066cc; text-decoration: none; @@ -61,6 +85,18 @@ body { text-decoration: underline; } +[data-theme="dark"] #footer { + color: #d4d4d4; +} + +[data-theme="dark"] #footer a { + color: #4fc3f7; +} + +[data-theme="dark"] #footer a:hover { + color: #81d4fa; +} + /* Footer toggle button */ #footer-toggle { display: none; diff --git a/docker/src/main.js b/docker/src/main.js index ec244d00..b572bacc 100644 --- a/docker/src/main.js +++ b/docker/src/main.js @@ -24,6 +24,12 @@ }); })(); +// Function to sync theme with body +function syncThemeWithBody() { + const theme = document.documentElement.getAttribute("data-theme") || "light"; + document.body.setAttribute("data-theme", theme); +} + // YASGUI is loaded via CDN in index.html and available as a global variable if (typeof Yasgui !== "undefined") { const yasgui = new Yasgui(document.getElementById("yasgui"), { @@ -71,4 +77,21 @@ if (typeof Yasgui !== "undefined") { }, }, }); + + // Sync theme immediately after initialization + syncThemeWithBody(); + + // Watch for theme changes on document.documentElement + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === "attributes" && mutation.attributeName === "data-theme") { + syncThemeWithBody(); + } + }); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-theme"], + }); }