Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/whats-new.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@matdata/yasgui": minor
---

**What's new**: the help button of the top bar now opens a menu with **What's new** and the **Documentation**. What's new lists the changes of each version since 5.0.0 as New, Improved or Fixed, with a filter per kind and a link to the full release notes. After an update a dot on the help button points to it until it is opened; first-time users get no dot. Also in the Ctrl+K search. Embedders can switch it off with `showWhatsNew: false` and add their own entries with `yasgui.helpMenu.addItem()`. `helpUrl: false` now only leaves out the documentation entry.
25 changes: 24 additions & 1 deletion docs/developer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -718,9 +718,14 @@ interface Config {
// Look up IRIs the endpoint doesn't describe in LOV (lov.linkeddata.es)
iriHoverLovFallback?: boolean; // default: true

// Page opened by the help button of the top bar; false hides the button
// Documentation link in the menu of the help button of the top bar; false leaves it out.
// The help button hides when its menu has no entries left
helpUrl?: string | false; // default: 'https://matgui-doc.matdata.eu/docs/user-guide'

// "What's new" in the help menu: the changes per version, with a dot on the help button
// until the user has seen the latest release. Also in the Ctrl+K search
showWhatsNew?: boolean; // default: true

// AI assistant (see AI Assistant Configuration); { enabled: false } removes it
ai: Partial<AiConfig>;
/**
Expand Down Expand Up @@ -2542,6 +2547,24 @@ The older entry points keep working and now drive the side panel:

A pinned describe panel saved by an earlier version is restored as the pinned Endpoint view.

#### Help menu and What's new

The help button of the top bar opens `yasgui.helpMenu`. Add your own entries, for example a link to your site's documentation; entries are sorted on `order` (built-in: quick start 10, What's new 20, Documentation 90, default 100). With a single entry the button runs it directly, and without entries it is hidden.

```javascript
yasgui.helpMenu.addItem({
id: "contact", // an existing id is replaced
label: "Contact support",
icon: "externalLink", // optional, a MatGUI icon name
order: 95,
onClick: () => window.open("https://example.org/support", "_blank"),
});
yasgui.helpMenu.setBadge("contact", true); // dot on the entry and the help button
yasgui.helpMenu.removeItem("contact");
```

`yasgui.whatsNew` (unless `showWhatsNew: false`) has `open()`, `close()`, `hasUnseen()` and `getLatestVersion()`. The release entries live in `packages/yasgui/src/whatsNew/releases.ts`, newest first; add the next version there when preparing a release. The newest version a user has opened is stored as `whatsNewSeen` in the persisted config.

### Tab Class

Represents a query tab.
Expand Down
16 changes: 14 additions & 2 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [The Interface](#the-interface)
- [Top Bar](#top-bar)
- [Search](#search)
- [Help Menu and What's New](#help-menu-and-whats-new)
- [Query Card](#query-card)
- [Endpoint Picker](#endpoint-picker)
- [Run and Stop](#run-and-stop)
Expand Down Expand Up @@ -352,7 +353,7 @@ From left to right:
- **MatGUI**: the name of the application (sites that embed MatGUI can hide it)
- **Query tabs**: one tab per query. The active tab is raised. Tabs that hold a saved (managed) query show a small dot before their name; the dot turns amber when the query has unsaved changes. Hover a tab to show its close button (×), click **+** to add a tab
- **Search queries and commands** (`Ctrl+K`, or `⌘K` on a Mac): see [Search](#search)
- **Help** (?): opens the MatGUI user guide in a new browser tab
- **Help** (?): opens the help menu with **What's new** and the **Documentation** (see [Help Menu and What's New](#help-menu-and-whats-new)). A blue dot on the button means there is a release you haven't seen yet
- **Theme** (sun / moon): switches between the light and dark theme
- **Settings** (gear): opens the settings of the active tab

Expand All @@ -362,11 +363,22 @@ Click **Search queries and commands** in the top bar, or press `Ctrl+K` (`⌘K`

- **Queries**: the saved queries of all your workspaces, with the workspace and folder they are in. Pick one to open it in a new tab, or to go to the tab that already has it open
- **Snippets**: the code snippets of the [Insert menu](#insert-menu), found by name, group or the code they write (`filter` finds the comparison snippets). Pick one to insert it at the cursor
- **Commands**: Run query, New tab, Insert snippet, Insert PREFIX declarations, Switch endpoint, Explore endpoint, Show saved queries, Toggle side panel, Toggle side-by-side layout, Format, Share, Save, Download, full screen editor, dark mode and the documentation. Commands with a shortcut show it at the right
- **Commands**: Run query, New tab, Insert snippet, Insert PREFIX declarations, Switch endpoint, Explore endpoint, Show saved queries, Toggle side panel, Toggle side-by-side layout, Format, Share, Save, Download, full screen editor, dark mode, What's new and the documentation. Commands with a shortcut show it at the right
- **Settings**: every page of the settings (General, SPARQL endpoints, Workspaces, Prefixes, Editor, AI, Import / export, Keyboard shortcuts, About), for example type `prefixes` and pick **Settings: Prefixes**

Type `>` first to search commands and settings only. Snippets and settings show up once you type something. Use `↑` and `↓` to move, `Enter` to open and `Esc` to close.

### Help Menu and What's New

Click **Help** (?) in the top bar to open the help menu:

- **What's new**: what changed in each version of MatGUI since 5.0.0, newest first. Every change is marked **New**, **Improved** or **Fixed**; use the buttons at the top to show only one kind. Releases you haven't seen yet are marked **New to you**. **Full release notes on GitHub** opens the complete notes, including the changes for developers
- **Documentation**: opens this user guide in a new browser tab

After MatGUI is updated, a blue dot on the Help button and next to **What's new** tells you there is something new. The dot goes away once you open What's new. On your first visit there is no dot. You can also open it from the [search](#search) (`Ctrl+K`, then type `what's new`). Press `Esc` to close it.

Sites that embed MatGUI can leave out What's new (`showWhatsNew: false`) or the documentation link (`helpUrl: false`).

### Query Card

The head of the card shows:
Expand Down
9 changes: 9 additions & 0 deletions packages/yasgui/src/CommandPalette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,15 @@ export default class CommandPalette {
},
});
}
if (yasgui.whatsNew) {
commands.push({
section: "commands",
label: "What's new",
icon: "sparkles",
keywords: "release notes changelog changes version update",
run: () => yasgui.whatsNew?.open(),
});
}
if (yasgui.config.helpUrl) {
const helpUrl = yasgui.config.helpUrl;
commands.push({
Expand Down
84 changes: 84 additions & 0 deletions packages/yasgui/src/HelpMenu.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// `.yasgui > div` lays out the tab panels as a column; the help menu is not one of them
.yasgui > div.yasgui-helpMenu:not(.tabsList) {
flex-direction: column;
overflow: visible;
min-height: 0;

&[hidden] {
display: none;
}
}

.yasgui .yasgui-helpButton {
position: relative;
}

.yasgui-helpButton__badge {
position: absolute;
top: 6px;
right: 6px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--yasgui-brand, #0563bb);
box-shadow: 0 0 0 2px var(--yasgui-surface, #ffffff);
pointer-events: none;
}

// Dropdown of the help button
.yasgui-helpMenu {
position: fixed;
z-index: 10040;
display: flex;
min-width: 200px;
padding: 4px;
box-sizing: border-box;
border-radius: 10px;
background: var(--yasgui-surface-raised, #ffffff);
box-shadow: var(--yasgui-shadow-overlay, 0 24px 48px rgba(13, 20, 28, 0.18), 0 0 0 1px rgba(13, 20, 28, 0.06));
font-family: var(--yasgui-font-family, inherit);
font-size: 13px;
color: var(--yasgui-ink, #1c2633);

&[hidden] {
display: none;
}

&__item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
height: 32px;
padding: 0 10px;
border: none;
border-radius: 6px;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;

.yasgui-icon {
flex-shrink: 0;
color: var(--yasgui-ink-subtle, #6a7380);
}

span:first-of-type {
flex: 1;
}

&:hover,
&:focus-visible {
outline: none;
background: var(--yasgui-surface-sunken, #edf0f4);
}
}

&__badge {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--yasgui-brand, #0563bb);
}
}
196 changes: 196 additions & 0 deletions packages/yasgui/src/HelpMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { addClass } from "@matdata/yasgui-utils";
import { icon, IconName } from "./icons";
import type Yasgui from "./";
import "./HelpMenu.scss";

export interface HelpMenuItem {
/** Unique id, used to replace, remove or badge the item later */
id: string;
label: string;
icon?: IconName;
onClick: () => void;
/** Show a dot on the item and on the help button, e.g. for unseen release notes */
badge?: boolean;
/** Items are sorted on this number (default 100); built-in items use 10, 20 and 90 */
order?: number;
}

/**
* Menu of the help button in the top bar: the quick start tour, what's new and the documentation.
* The button only shows when the menu has an item; with a single item it runs that item directly.
*/
export default class HelpMenu {
private yasgui: Yasgui;
private items: HelpMenuItem[] = [];
private buttonEl?: HTMLButtonElement;
private menuEl?: HTMLDivElement;

constructor(yasgui: Yasgui) {
this.yasgui = yasgui;
const helpUrl = yasgui.config.helpUrl;
if (helpUrl) {
this.addItem({
id: "documentation",
label: "Documentation",
icon: "externalLink",
order: 90,
onClick: () => window.open(helpUrl, "_blank", "noopener,noreferrer"),
});
}
}

/** Add an item, or replace the item with the same id */
public addItem(item: HelpMenuItem) {
this.items = this.items.filter((existing) => existing.id !== item.id);
this.items.push(item);
this.items.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
this.update();
}

public removeItem(id: string) {
this.items = this.items.filter((item) => item.id !== id);
this.update();
}

public setBadge(id: string, badge: boolean) {
const item = this.items.find((existing) => existing.id === id);
if (!item || !!item.badge === badge) return;
item.badge = badge;
this.update();
}

public getItems(): HelpMenuItem[] {
return [...this.items];
}

/** The help button of the top bar, kept up to date with the items */
public getButton(): HTMLButtonElement {
if (this.buttonEl) return this.buttonEl;
const button = document.createElement("button");
button.type = "button";
button.className = "yasgui-topBarButton yasgui-helpButton";
button.setAttribute("aria-haspopup", "menu");
button.setAttribute("aria-expanded", "false");
button.addEventListener("click", (event) => {
event.stopPropagation();
if (this.items.length === 1) {
this.items[0].onClick();
return;
}
if (this.isOpen()) this.close();
else this.open();
});
this.buttonEl = button;
this.update();
return button;
}

public isOpen(): boolean {
return !!this.menuEl && !this.menuEl.hidden;
}

public open() {
if (!this.buttonEl || !this.items.length) return;
if (!this.menuEl) this.drawMenu();
const menu = this.menuEl!;
this.renderMenu();
menu.hidden = false;
this.buttonEl.setAttribute("aria-expanded", "true");
const rect = this.buttonEl.getBoundingClientRect();
menu.style.top = `${rect.bottom + 6}px`;
menu.style.right = `${Math.max(8, document.documentElement.clientWidth - rect.right)}px`;
(menu.querySelector("button") as HTMLButtonElement | null)?.focus();
document.addEventListener("mousedown", this.handleOutside, true);
document.addEventListener("keydown", this.handleKeydown, true);
}

public close(returnFocus = false) {
if (!this.menuEl || this.menuEl.hidden) return;
this.menuEl.hidden = true;
this.buttonEl?.setAttribute("aria-expanded", "false");
document.removeEventListener("mousedown", this.handleOutside, true);
document.removeEventListener("keydown", this.handleKeydown, true);
if (returnFocus) this.buttonEl?.focus();
}

public destroy() {
this.close();
this.menuEl?.remove();
this.menuEl = undefined;
}

private handleOutside = (event: MouseEvent) => {
const target = event.target as Node | null;
if (target && (this.menuEl?.contains(target) || this.buttonEl?.contains(target))) return;
this.close();
};

private handleKeydown = (event: KeyboardEvent) => {
if (!this.menuEl) return;
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
this.close(true);
return;
}
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
event.preventDefault();
const buttons = Array.from(this.menuEl.querySelectorAll<HTMLButtonElement>("button"));
const current = buttons.indexOf(document.activeElement as HTMLButtonElement);
const next = (current + (event.key === "ArrowDown" ? 1 : -1) + buttons.length) % buttons.length;
buttons[next]?.focus();
};

private drawMenu() {
const menu = document.createElement("div");
addClass(menu, "yasgui-helpMenu");
menu.setAttribute("role", "menu");
menu.hidden = true;
// Inside the Yasgui root, so it gets the theme tokens
this.yasgui.rootEl.appendChild(menu);
this.menuEl = menu;
}

private renderMenu() {
if (!this.menuEl) return;
this.menuEl.innerHTML = "";
for (const item of this.items) {
const entry = document.createElement("button");
entry.type = "button";
entry.setAttribute("role", "menuitem");
addClass(entry, "yasgui-helpMenu__item");
entry.innerHTML = `${item.icon ? icon(item.icon, 15) : ""}<span></span>`;
entry.querySelector("span")!.textContent = item.label;
if (item.badge) {
const dot = document.createElement("span");
addClass(dot, "yasgui-helpMenu__badge");
dot.setAttribute("aria-label", "New");
entry.appendChild(dot);
}
entry.addEventListener("click", () => {
this.close();
item.onClick();
});
this.menuEl.appendChild(entry);
}
}

private update() {
const button = this.buttonEl;
if (!button) return;
button.hidden = this.items.length === 0;
const single = this.items.length === 1 ? this.items[0] : undefined;
const badge = this.items.some((item) => item.badge);
const label = single?.id === "documentation" ? "Open the documentation" : single ? single.label : "Help";
button.title = badge ? `${label} (new)` : label;
button.setAttribute("aria-label", badge ? `${label}, new` : label);
button.setAttribute("aria-haspopup", single ? "false" : "menu");
button.innerHTML = icon("help", 16);
if (badge) {
const dot = document.createElement("span");
addClass(dot, "yasgui-helpButton__badge");
button.appendChild(dot);
}
if (this.isOpen()) this.renderMenu();
}
}
Loading
Loading