diff --git a/.changeset/quick-start-tour.md b/.changeset/quick-start-tour.md
new file mode 100644
index 00000000..6f9275e2
--- /dev/null
+++ b/.changeset/quick-start-tour.md
@@ -0,0 +1,5 @@
+---
+"@matdata/yasgui": minor
+---
+
+"How MatGUI works": a quick start tour of five pages, each with three things to know and a small illustration, opens the first time someone uses MatGUI and stays available from the help menu and the Ctrl+K search. It links to the full user guide. Embedders can leave it out with `showQuickStart: false`, or keep it in the help menu only with `quickStartOnFirstVisit: false`.
diff --git a/docs/developer-guide.md b/docs/developer-guide.md
index 9d5435fc..962b7dba 100644
--- a/docs/developer-guide.md
+++ b/docs/developer-guide.md
@@ -701,6 +701,13 @@ interface Config {
// Search for saved queries, commands and settings in the top bar, also opened with Ctrl+K / Cmd+K
showCommandPalette?: boolean; // default: true
+ // "How MatGUI works" quick start tour in the help menu of the top bar
+ showQuickStart?: boolean; // default: true
+
+ // Open the quick start tour on the first visit in a browser (remembered in local storage under
+ // "matgui_quickStart_seen"). Set to false to only offer it in the help menu
+ quickStartOnFirstVisit?: boolean; // default: true
+
// Layout orientation: 'vertical' or 'horizontal'
orientation?: 'vertical' | 'horizontal'; // default: 'vertical'
diff --git a/docs/user-guide.md b/docs/user-guide.md
index f62d5374..890bb24f 100644
--- a/docs/user-guide.md
+++ b/docs/user-guide.md
@@ -14,6 +14,7 @@
- [Common SPARQL Query Types](#common-sparql-query-types)
- [Getting Started](#getting-started)
- [Accessing MatGUI](#accessing-matgui)
+ - [Quick Start Tour](#quick-start-tour)
- [Running Matgui with Docker](#running-matgui-with-docker)
- [Your First Query](#your-first-query)
- [Saving Your Work](#saving-your-work)
@@ -150,6 +151,20 @@ This excellent resource covers:
Matgui is available at: **[https://matgui.matdata.eu/](https://matgui.matdata.eu/)**
+### Quick Start Tour
+
+The first time you open MatGUI in a browser, **How MatGUI works** shows you around in five short pages, with three things to know on each:
+
+1. **Write and run a query**: pick an endpoint, write with autocomplete, run with `Ctrl+Enter`
+2. **Explore the results**: switch views, describe a resource, download the results
+3. **Keep your queries**: work in tabs, save to a workspace, share a link
+4. **Let MatGUI help**: insert snippets, look at the endpoint, ask the AI assistant (or hover an IRI, when the site has no AI assistant)
+5. **Find your way**: search everything with `Ctrl+K`, open the side panel with `Ctrl+B`, switch the theme and open the settings
+
+Click **Next** and **Back**, the dots at the bottom, or use the `←` and `→` keys to move between the pages. **Read the full user guide** opens this guide. Close the tour with **Skip**, **Start querying** on the last page, the × or `Esc`. MatGUI remembers in your browser that you saw it and doesn't open it again by itself.
+
+To see it again, click **Help** (?) in the top bar and pick **How MatGUI works**, or search for it with `Ctrl+K`. Sites that embed MatGUI can leave the tour out, or keep it in the help menu without opening it on the first visit.
+
### Running Matgui with Docker
You can run Matgui locally using Docker, which is especially useful when working with local SPARQL endpoints.
@@ -353,7 +368,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 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
+- **Help** (?): opens the help menu with **How MatGUI works** (the [quick start tour](#quick-start-tour)), **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
@@ -363,7 +378,7 @@ 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, What's new 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, How MatGUI works, 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.
@@ -372,12 +387,13 @@ Type `>` first to search commands and settings only. Snippets and settings show
Click **Help** (?) in the top bar to open the help menu:
+- **How MatGUI works**: the [quick start tour](#quick-start-tour), five short pages that show you around
- **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`).
+Sites that embed MatGUI can leave out the quick start tour (`showQuickStart: false`), What's new (`showWhatsNew: false`) or the documentation link (`helpUrl: false`).
### Query Card
diff --git a/packages/yasgui/src/CommandPalette.ts b/packages/yasgui/src/CommandPalette.ts
index 86661586..e1573115 100644
--- a/packages/yasgui/src/CommandPalette.ts
+++ b/packages/yasgui/src/CommandPalette.ts
@@ -386,6 +386,18 @@ export default class CommandPalette {
},
});
}
+ if (yasgui.quickStart) {
+ const quickStart = yasgui.quickStart;
+ commands.push({
+ section: "commands",
+ label: "How MatGUI works",
+ icon: "compass",
+ keywords: "help quick start tour introduction onboarding",
+ run: () => {
+ quickStart.open();
+ },
+ });
+ }
if (yasgui.whatsNew) {
commands.push({
section: "commands",
diff --git a/packages/yasgui/src/QuickStart.scss b/packages/yasgui/src/QuickStart.scss
new file mode 100644
index 00000000..807efa42
--- /dev/null
+++ b/packages/yasgui/src/QuickStart.scss
@@ -0,0 +1,423 @@
+// `.yasgui > div` lays out the tab panels as a column; the quick start is not one of them
+.yasgui > div.yasgui-quickStart:not(.tabsList) {
+ flex-direction: row;
+ overflow: auto;
+ min-height: 0;
+
+ &[hidden] {
+ display: none;
+ }
+}
+
+// "How MatGUI works": the quick start tour, opened on the first visit and from the help menu
+.yasgui-quickStart {
+ // Colours of the illustrations
+ --qs-keyword: #a3367f;
+ --qs-iri: #0563bb;
+ --qs-variable: #2f7d4f;
+ --qs-folder: #f2b300;
+
+ position: fixed;
+ inset: 0;
+ z-index: 10060;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 24px 16px;
+ box-sizing: border-box;
+ background: var(--yasgui-scrim, rgba(28, 38, 51, 0.28));
+ font-family: var(--yasgui-font-family, inherit);
+ font-size: 13px;
+ color: var(--yasgui-ink, #1c2633);
+
+ &[hidden] {
+ display: none;
+ }
+
+ [data-theme="dark"] & {
+ --qs-keyword: #e48fc9;
+ --qs-iri: #5aa8f2;
+ --qs-variable: #6fcf97;
+ }
+
+ &__dialog {
+ position: relative;
+ width: 760px;
+ max-width: 100%;
+ max-height: 100%;
+ display: flex;
+ flex-direction: column;
+ overflow: auto;
+ border-radius: var(--yasgui-radius-lg, 16px);
+ background: var(--yasgui-surface-raised, #ffffff);
+ box-shadow: var(--yasgui-shadow-overlay, 0 16px 48px rgba(28, 38, 51, 0.22), 0 0 0 1px rgba(28, 38, 51, 0.06));
+ outline: none;
+ }
+
+ &__header {
+ position: relative;
+ padding: 24px 56px 4px 28px;
+ }
+
+ &__eyebrow {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--yasgui-brand, #0563bb);
+ }
+
+ &__title {
+ margin: 6px 0 0;
+ font-family: inherit;
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 1.3;
+ letter-spacing: -0.01em;
+ color: var(--yasgui-ink, #1c2633);
+ }
+
+ &__lead {
+ margin: 6px 0 0;
+ font-size: 13px;
+ line-height: 1.5;
+ color: var(--yasgui-ink-muted, #56606c);
+ }
+
+ &__close {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ border-radius: var(--yasgui-radius-button, 8px);
+ background: transparent;
+ color: var(--yasgui-ink-muted, #56606c);
+ cursor: pointer;
+
+ &:hover {
+ background: var(--yasgui-surface-sunken, #edf1f6);
+ color: var(--yasgui-ink, #1c2633);
+ }
+ }
+
+ &__items {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 16px;
+ margin: 0;
+ padding: 20px 28px 24px;
+ list-style: none;
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin: 0;
+
+ h3 {
+ margin: 0;
+ font-family: inherit;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 1.35;
+ color: var(--yasgui-ink, #1c2633);
+ }
+
+ p {
+ margin: 4px 0 0;
+ font-size: 13px;
+ line-height: 1.5;
+ color: var(--yasgui-ink-body, #3a4552);
+ }
+ }
+
+ &__illustration {
+ height: 112px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--yasgui-radius-md, 10px);
+ background: var(--yasgui-surface, #f1f4f8);
+ box-shadow: inset 0 0 0 1px var(--yasgui-line-soft, #edf0f4);
+
+ svg {
+ width: 150px;
+ height: 90px;
+ overflow: visible;
+ }
+ }
+
+ &__footer {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 14px 20px 14px 28px;
+ border-top: 1px solid var(--yasgui-line-soft, #edf0f4);
+ }
+
+ &__dots {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ }
+
+ &__dot {
+ width: 18px;
+ height: 18px;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ border-radius: 50%;
+ background: transparent;
+ cursor: pointer;
+
+ &::before {
+ content: "";
+ width: 7px;
+ height: 7px;
+ border-radius: 999px;
+ background: var(--yasgui-line, #dfe5ec);
+ transition:
+ width 0.15s ease,
+ background-color 0.15s ease;
+ }
+
+ &:hover::before {
+ background: var(--yasgui-line-strong, #808893);
+ }
+
+ &.active {
+ width: 26px;
+ border-radius: 999px;
+
+ &::before {
+ width: 18px;
+ background: var(--yasgui-brand, #0563bb);
+ }
+ }
+ }
+
+ &__guide {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ margin-right: auto;
+ color: var(--yasgui-brand, #0563bb) !important;
+ font-weight: 500;
+ text-decoration: none;
+
+ &:hover span {
+ text-decoration: underline;
+ }
+ }
+
+ &__nav {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-left: auto;
+ }
+
+ &__button {
+ height: 34px;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 0 14px;
+ border: none;
+ border-radius: var(--yasgui-radius-button, 8px);
+ background: transparent;
+ color: var(--yasgui-ink-body, #3a4552);
+ font: inherit;
+ font-weight: 500;
+ cursor: pointer;
+
+ &:hover {
+ background: var(--yasgui-surface-sunken, #edf1f6);
+ color: var(--yasgui-ink, #1c2633);
+ }
+
+ &.primary {
+ padding: 0 12px 0 16px;
+ background: var(--yasgui-brand-fill, var(--yasgui-brand, #0563bb));
+ color: var(--yasgui-on-brand, #ffffff);
+
+ &:hover {
+ background: var(--yasgui-brand-fill-strong, var(--yasgui-brand-strong, #044a8c));
+ color: var(--yasgui-on-brand, #ffffff);
+ }
+ }
+
+ &.primary:not(:has(svg)) {
+ padding: 0 16px;
+ }
+ }
+
+ button:focus-visible,
+ a:focus-visible {
+ outline: 2px solid var(--yasgui-focus, #044a8c);
+ outline-offset: 2px;
+ }
+
+ // Illustration parts
+ .qs-card {
+ fill: var(--yasgui-surface-raised, #ffffff);
+ stroke: var(--yasgui-line, #dfe5ec);
+ stroke-width: 1;
+ }
+ .qs-raised {
+ filter: drop-shadow(0 2px 3px rgba(28, 38, 51, 0.1));
+ }
+ .qs-darkCard {
+ fill: #141920;
+ stroke: var(--yasgui-line, #dfe5ec);
+ }
+ .qs-sunken,
+ .qs-sunkenFill {
+ fill: var(--yasgui-surface-sunken, #edf1f6);
+ }
+ .qs-kbd {
+ fill: var(--yasgui-surface-sunken, #edf1f6);
+ stroke: var(--yasgui-line, #dfe5ec);
+ }
+ .qs-text {
+ fill: var(--yasgui-line, #dfe5ec);
+ }
+ .qs-ink {
+ fill: var(--yasgui-ink-subtle, #6a7380);
+ }
+ .qs-line {
+ stroke: var(--yasgui-line-soft, #edf0f4);
+ stroke-width: 1;
+ }
+ .qs-stroke {
+ stroke: var(--yasgui-ink-subtle, #6a7380);
+ stroke-width: 1.5;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ }
+ .qs-brand,
+ .qs-brandFill {
+ fill: var(--yasgui-brand-fill, var(--yasgui-brand, #0563bb));
+ }
+ .qs-brandMid {
+ fill: var(--yasgui-brand, #0563bb);
+ opacity: 0.6;
+ }
+ .qs-brandSoft {
+ fill: var(--yasgui-brand-soft, #e3eefa);
+ }
+ .qs-brandSoftStrong {
+ fill: var(--yasgui-brand, #0563bb);
+ opacity: 0.3;
+ }
+ .qs-brandInk {
+ fill: var(--yasgui-brand, #0563bb);
+ }
+ .qs-brandStroke {
+ stroke: var(--yasgui-brand, #0563bb);
+ stroke-width: 1.5;
+ stroke-linecap: round;
+ }
+ .qs-onBrand,
+ .qs-onBrandBar {
+ fill: var(--yasgui-on-brand, #ffffff);
+ }
+ .qs-onBrandBar {
+ opacity: 0.85;
+ }
+ .qs-onBrandStroke {
+ stroke: var(--yasgui-on-brand, #ffffff);
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ }
+ .qs-keyword {
+ fill: var(--qs-keyword);
+ }
+ .qs-iri {
+ fill: var(--qs-iri);
+ }
+ .qs-iriSoft {
+ fill: var(--yasgui-brand-soft, #e3eefa);
+ }
+ .qs-variable {
+ fill: var(--qs-variable);
+ }
+ .qs-caret {
+ fill: var(--yasgui-ink, #1c2633);
+ }
+ .qs-cursor {
+ fill: var(--yasgui-ink, #1c2633);
+ stroke: var(--yasgui-surface-raised, #ffffff);
+ stroke-width: 1;
+ stroke-linejoin: round;
+ }
+ .qs-folder {
+ fill: var(--qs-folder);
+ }
+ .qs-file {
+ fill: var(--yasgui-brand, #0563bb);
+ }
+ .qs-highlight {
+ fill: var(--yasgui-highlight, #f2b300);
+ }
+ .qs-highlightStroke {
+ stroke: var(--yasgui-highlight, #f2b300);
+ stroke-width: 2;
+ stroke-linecap: round;
+ }
+ .qs-moon {
+ fill: #c9d2dc;
+ }
+
+ @media (max-width: 680px) {
+ align-items: flex-end;
+ padding: 12px;
+
+ &__header {
+ padding: 20px 52px 0 20px;
+ }
+
+ &__items {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 14px;
+ padding: 16px 20px 20px;
+ }
+
+ &__item {
+ flex-direction: row;
+ align-items: flex-start;
+ gap: 14px;
+ }
+
+ &__illustration {
+ flex-shrink: 0;
+ width: 96px;
+ height: 64px;
+
+ svg {
+ width: 84px;
+ height: 50px;
+ }
+ }
+
+ &__footer {
+ flex-wrap: wrap;
+ padding: 12px 16px 12px 20px;
+ row-gap: 8px;
+ }
+
+ &__guide {
+ order: 3;
+ width: 100%;
+ }
+ }
+}
diff --git a/packages/yasgui/src/QuickStart.ts b/packages/yasgui/src/QuickStart.ts
new file mode 100644
index 00000000..57c9367f
--- /dev/null
+++ b/packages/yasgui/src/QuickStart.ts
@@ -0,0 +1,587 @@
+import { addClass } from "@matdata/yasgui-utils";
+import { icon, modKeyLabel } from "./icons";
+import type Yasgui from "./";
+import "./QuickStart.scss";
+
+/** Raise when the tour changes enough to show it again to users who saw an earlier one */
+const TOUR_VERSION = "1";
+const SEEN_KEY = "matgui_quickStart_seen";
+
+interface TourItem {
+ title: string;
+ text: string;
+ /** Inline SVG drawn on a 120 x 72 canvas, coloured with the `qs-*` classes */
+ illustration: string;
+}
+
+interface TourPage {
+ title: string;
+ lead: string;
+ items: TourItem[];
+}
+
+// Small, schematic drawings of the interface. The classes map to theme tokens in QuickStart.scss.
+const svg = (body: string) =>
+ ``;
+const card = (x: number, y: number, w: number, h: number, extra = "") =>
+ ``;
+const bar = (x: number, y: number, w: number, cls = "qs-text") =>
+ ``;
+
+const ILLUSTRATIONS = {
+ endpoint: svg(
+ card(14, 8, 92, 18) +
+ `` +
+ bar(34, 15, 44, "qs-ink") +
+ `` +
+ card(14, 30, 92, 36, ' class="qs-card qs-raised"') +
+ `` +
+ bar(24, 38, 50, "qs-brandInk") +
+ bar(24, 54, 40),
+ ),
+ autocomplete: svg(
+ card(6, 6, 108, 60) +
+ bar(14, 14, 18, "qs-keyword") +
+ bar(35, 14, 30, "qs-iri") +
+ bar(14, 24, 10, "qs-variable") +
+ bar(27, 24, 16, "qs-ink") +
+ `` +
+ card(40, 32, 66, 30, ' class="qs-card qs-raised"') +
+ `` +
+ bar(47, 37, 36, "qs-brandInk") +
+ bar(47, 47, 44) +
+ bar(47, 55, 30),
+ ),
+ run: svg(
+ card(6, 14, 108, 44) +
+ `` +
+ `` +
+ bar(42, 34, 22, "qs-onBrandBar") +
+ `` +
+ bar(85, 34, 14),
+ ),
+ views: svg(
+ card(6, 6, 108, 60) +
+ `` +
+ `` +
+ bar(18, 16, 16, "qs-ink") +
+ bar(44, 16, 16) +
+ bar(66, 16, 16) +
+ bar(88, 16, 14) +
+ `` +
+ bar(16, 37, 20, "qs-iri") +
+ bar(48, 37, 22) +
+ bar(16, 47, 24, "qs-iri") +
+ bar(48, 47, 18) +
+ bar(80, 37, 20) +
+ bar(80, 47, 16),
+ ),
+ describe: svg(
+ card(6, 6, 108, 60) +
+ `` +
+ bar(14, 16, 34, "qs-iri") +
+ bar(14, 28, 40) +
+ bar(14, 40, 30) +
+ `` +
+ bar(71, 16, 28, "qs-ink") +
+ bar(71, 23, 20, "qs-brandInk") +
+ bar(71, 34, 34) +
+ bar(71, 42, 26) +
+ bar(71, 50, 30) +
+ ``,
+ ),
+ download: svg(
+ `` +
+ `` +
+ bar(42, 30, 28) +
+ bar(42, 38, 22) +
+ `` +
+ ``,
+ ),
+ tabs: svg(
+ `` +
+ `` +
+ `` +
+ bar(21, 23, 18, "qs-ink") +
+ bar(50, 23, 22) +
+ bar(78, 23, 16) +
+ `` +
+ card(10, 40, 100, 26) +
+ bar(16, 47, 20, "qs-keyword") +
+ bar(39, 47, 34, "qs-iri") +
+ bar(16, 56, 46),
+ ),
+ workspaces: svg(
+ card(10, 6, 100, 60) +
+ `` +
+ bar(41, 17, 36, "qs-ink") +
+ `` +
+ bar(49, 33, 30, "qs-ink") +
+ `` +
+ `` +
+ bar(49, 49, 32, "qs-brandInk"),
+ ),
+ share: svg(
+ card(6, 18, 108, 24) +
+ `` +
+ bar(32, 28, 54) +
+ `` +
+ `` +
+ `` +
+ bar(34, 54, 52, "qs-brandInk"),
+ ),
+ snippets: svg(
+ card(6, 6, 50, 18) +
+ `` +
+ bar(33, 13, 16, "qs-ink") +
+ card(6, 28, 108, 38, ' class="qs-card qs-raised"') +
+ `` +
+ bar(20, 36, 40, "qs-ink") +
+ `` +
+ bar(20, 47, 52, "qs-brandInk") +
+ bar(20, 58, 36),
+ ),
+ overview: svg(
+ card(6, 6, 108, 60) +
+ `` +
+ bar(22, 13, 40, "qs-ink") +
+ bar(12, 27, 30) +
+ `` +
+ bar(12, 39, 24) +
+ `` +
+ bar(12, 51, 28) +
+ ``,
+ ),
+ ai: svg(
+ `` +
+ bar(16, 16, 50, "qs-ink") +
+ card(30, 34, 82, 32, ' class="qs-card qs-raised"') +
+ `` +
+ bar(50, 42, 54, "qs-keyword") +
+ bar(50, 50, 40, "qs-iri") +
+ bar(38, 58, 52),
+ ),
+ hover: svg(
+ card(6, 40, 108, 26) +
+ bar(14, 48, 18, "qs-keyword") +
+ `` +
+ bar(36, 48, 36, "qs-iri") +
+ bar(14, 58, 50) +
+ card(22, 4, 76, 34, ' class="qs-card qs-raised"') +
+ bar(29, 11, 32, "qs-ink") +
+ bar(29, 19, 56) +
+ bar(29, 27, 40) +
+ ``,
+ ),
+ search: svg(
+ card(8, 14, 104, 22, ' class="qs-card qs-raised"') +
+ `` +
+ bar(32, 23, 40) +
+ `` +
+ bar(88, 23, 14) +
+ `` +
+ bar(14, 45, 46, "qs-brandInk") +
+ bar(14, 60, 60),
+ ),
+ keys: svg(
+ `` +
+ bar(18, 34, 14, "qs-ink") +
+ `` +
+ bar(53, 34, 14, "qs-ink") +
+ `` +
+ bar(88, 34, 14, "qs-onBrandBar"),
+ ),
+ sidePanel: svg(
+ card(6, 6, 108, 60) +
+ `` +
+ `` +
+ `` +
+ `` +
+ bar(27, 13, 26, "qs-ink") +
+ bar(27, 24, 30) +
+ bar(27, 32, 22) +
+ bar(27, 40, 28) +
+ bar(68, 13, 20, "qs-keyword") +
+ bar(68, 22, 36, "qs-iri") +
+ bar(68, 31, 28),
+ ),
+ settings: svg(
+ `` +
+ `` +
+ `` +
+ `` +
+ ``,
+ ),
+};
+
+export default class QuickStart {
+ private yasgui: Yasgui;
+ private overlayEl?: HTMLDivElement;
+ private dialogEl?: HTMLDivElement;
+ private pages: TourPage[] = [];
+ private pageIndex = 0;
+ private returnFocusEl?: HTMLElement;
+
+ constructor(yasgui: Yasgui) {
+ this.yasgui = yasgui;
+ yasgui.helpMenu.addItem({
+ id: "quickStart",
+ label: "How MatGUI works",
+ icon: "compass",
+ order: 10,
+ onClick: () => this.open(),
+ });
+ }
+
+ /** Whether this browser has seen the current version of the tour */
+ public static hasBeenSeen(): boolean {
+ try {
+ return window.localStorage.getItem(SEEN_KEY) === TOUR_VERSION;
+ } catch {
+ // Without storage we can't remember a dismissal, so don't show the tour on every visit
+ return true;
+ }
+ }
+
+ public static markSeen() {
+ try {
+ window.localStorage.setItem(SEEN_KEY, TOUR_VERSION);
+ } catch {
+ // ignore: storage is blocked or full
+ }
+ }
+
+ public isOpen(): boolean {
+ return !!this.overlayEl && !this.overlayEl.hidden;
+ }
+
+ public open(page = 0) {
+ QuickStart.markSeen();
+ this.pages = this.buildPages();
+ if (!this.overlayEl) this.draw();
+ if (!this.overlayEl) return;
+ if (!this.isOpen() && document.activeElement instanceof HTMLElement) {
+ this.returnFocusEl = document.activeElement;
+ }
+ this.overlayEl.hidden = false;
+ document.addEventListener("keydown", this.handleKeydown, true);
+ this.showPage(page, false);
+ this.dialogEl?.focus();
+ }
+
+ public close() {
+ if (!this.overlayEl || this.overlayEl.hidden) return;
+ this.overlayEl.hidden = true;
+ document.removeEventListener("keydown", this.handleKeydown, true);
+ if (this.returnFocusEl?.isConnected) this.returnFocusEl.focus();
+ this.returnFocusEl = undefined;
+ }
+
+ public destroy() {
+ this.close();
+ this.overlayEl?.remove();
+ this.overlayEl = undefined;
+ this.yasgui.helpMenu.removeItem("quickStart");
+ }
+
+ public getPageCount(): number {
+ return this.buildPages().length;
+ }
+
+ private buildPages(): TourPage[] {
+ const mod = modKeyLabel();
+ const yasgui = this.yasgui;
+ return [
+ {
+ title: "Write and run a query",
+ lead: "Every tab holds one SPARQL query and the endpoint it runs against.",
+ items: [
+ {
+ title: "Pick an endpoint",
+ text: "Click the endpoint at the top of the card to switch, or type the URL of any SPARQL endpoint.",
+ illustration: ILLUSTRATIONS.endpoint,
+ },
+ {
+ title: "Write with autocomplete",
+ text: `Prefixes, classes and properties are suggested while you type. Press Ctrl+Space to ask for them.`,
+ illustration: ILLUSTRATIONS.autocomplete,
+ },
+ {
+ title: "Run it",
+ text: `Click Run or press ${mod}+Enter. While the query runs, the button turns into Stop.`,
+ illustration: ILLUSTRATIONS.run,
+ },
+ ],
+ },
+ {
+ title: "Explore the results",
+ lead: "Results show up below the editor, or next to it in the side-by-side layout.",
+ items: [
+ {
+ title: "Switch views",
+ text: "See the results as a table, the raw response, or a map, graph or chart when the data fits.",
+ illustration: ILLUSTRATIONS.views,
+ },
+ {
+ title: "Describe a resource",
+ text: "Ctrl+click an IRI anywhere to open its properties in the describe panel, then click on to follow links.",
+ illustration: ILLUSTRATIONS.describe,
+ },
+ {
+ title: "Download the results",
+ text: "Each view has its own download, such as CSV for the table, in the toolbar above the results.",
+ illustration: ILLUSTRATIONS.download,
+ },
+ ],
+ },
+ {
+ title: "Keep your queries",
+ lead: "Your tabs, queries and results stay in this browser until you close them.",
+ items: [
+ {
+ title: "Work in tabs",
+ text: "Click + in the top bar for a new query. Double-click the title of the card to rename it.",
+ illustration: ILLUSTRATIONS.tabs,
+ },
+ {
+ title: "Save to a workspace",
+ text: "Keep queries in a Git repository or a SPARQL store and open them from the Queries view.",
+ illustration: ILLUSTRATIONS.workspaces,
+ },
+ {
+ title: "Share a link",
+ text: "Share copies a link with the query and the endpoint, so others open exactly what you see.",
+ illustration: ILLUSTRATIONS.share,
+ },
+ ],
+ },
+ {
+ title: "Let MatGUI help",
+ lead: "You don't need to remember every prefix or the shape of the data.",
+ items: [
+ {
+ title: "Insert snippets",
+ text: "Insert adds ready-made patterns, like FILTER or OPTIONAL, and Prefixes adds your declarations.",
+ illustration: ILLUSTRATIONS.snippets,
+ },
+ {
+ title: "Look at the endpoint",
+ text: "The Endpoint view in the side panel lists the classes and properties the endpoint holds.",
+ illustration: ILLUSTRATIONS.overview,
+ },
+ yasgui.aiAssistant
+ ? {
+ title: "Ask the AI assistant",
+ text: "Describe what you are looking for and get a query, or have a query explained or fixed.",
+ illustration: ILLUSTRATIONS.ai,
+ }
+ : {
+ title: "Hover an IRI",
+ text: "Hover an IRI in the editor to see its label, description and type.",
+ illustration: ILLUSTRATIONS.hover,
+ },
+ ],
+ },
+ {
+ title: "Find your way",
+ lead: "A few shortcuts get you anywhere in MatGUI.",
+ items: [
+ yasgui.commandPalette
+ ? {
+ title: "Search everything",
+ text: `Press ${mod}+K to find saved queries, snippets, commands and settings.`,
+ illustration: ILLUSTRATIONS.search,
+ }
+ : {
+ title: "Use the shortcuts",
+ text: "Settings lists every keyboard shortcut, like F11 for a full screen editor.",
+ illustration: ILLUSTRATIONS.keys,
+ },
+ {
+ title: "Open the side panel",
+ text: `Press ${mod}+B to show or hide your saved queries and the endpoint overview.`,
+ illustration: ILLUSTRATIONS.sidePanel,
+ },
+ {
+ title: "Make it yours",
+ text: "Switch between light and dark, and manage endpoints, prefixes and the editor in Settings.",
+ illustration: ILLUSTRATIONS.settings,
+ },
+ ],
+ },
+ ];
+ }
+
+ private draw() {
+ const overlay = document.createElement("div");
+ addClass(overlay, "yasgui-quickStart");
+ overlay.hidden = true;
+ overlay.addEventListener("mousedown", (event) => {
+ if (event.target === overlay) this.close();
+ });
+
+ const dialog = document.createElement("div");
+ addClass(dialog, "yasgui-quickStart__dialog");
+ dialog.setAttribute("role", "dialog");
+ dialog.setAttribute("aria-modal", "true");
+ dialog.setAttribute("aria-labelledby", "yasgui-quickStart-title");
+ dialog.tabIndex = -1;
+ this.dialogEl = dialog;
+ overlay.appendChild(dialog);
+
+ // Inside the Yasgui root, so it gets the theme tokens
+ this.yasgui.rootEl.appendChild(overlay);
+ this.overlayEl = overlay;
+ }
+
+ private showPage(index: number, focusNav = true) {
+ const dialog = this.dialogEl;
+ if (!dialog) return;
+ const count = this.pages.length;
+ this.pageIndex = Math.max(0, Math.min(index, count - 1));
+ const page = this.pages[this.pageIndex];
+ const last = this.pageIndex === count - 1;
+ const helpUrl = this.yasgui.config.helpUrl;
+
+ dialog.innerHTML = "";
+
+ const header = document.createElement("div");
+ addClass(header, "yasgui-quickStart__header");
+ const eyebrow = document.createElement("div");
+ addClass(eyebrow, "yasgui-quickStart__eyebrow");
+ eyebrow.textContent = `How MatGUI works · ${this.pageIndex + 1} of ${count}`;
+ const title = document.createElement("h2");
+ title.id = "yasgui-quickStart-title";
+ addClass(title, "yasgui-quickStart__title");
+ title.textContent = page.title;
+ const lead = document.createElement("p");
+ addClass(lead, "yasgui-quickStart__lead");
+ lead.textContent = page.lead;
+ const close = document.createElement("button");
+ close.type = "button";
+ addClass(close, "yasgui-quickStart__close");
+ close.innerHTML = icon("x", 16);
+ close.title = "Close";
+ close.setAttribute("aria-label", "Close the quick start");
+ close.addEventListener("click", () => this.close());
+ header.append(eyebrow, title, lead, close);
+ dialog.appendChild(header);
+
+ const items = document.createElement("ul");
+ addClass(items, "yasgui-quickStart__items");
+ for (const item of page.items) {
+ const entry = document.createElement("li");
+ addClass(entry, "yasgui-quickStart__item");
+ const figure = document.createElement("div");
+ addClass(figure, "yasgui-quickStart__illustration");
+ figure.innerHTML = item.illustration;
+ const body = document.createElement("div");
+ const heading = document.createElement("h3");
+ heading.textContent = item.title;
+ const text = document.createElement("p");
+ text.textContent = item.text;
+ body.append(heading, text);
+ entry.append(figure, body);
+ items.appendChild(entry);
+ }
+ dialog.appendChild(items);
+
+ const footer = document.createElement("div");
+ addClass(footer, "yasgui-quickStart__footer");
+
+ const dots = document.createElement("div");
+ addClass(dots, "yasgui-quickStart__dots");
+ this.pages.forEach((p, i) => {
+ const dot = document.createElement("button");
+ dot.type = "button";
+ addClass(dot, "yasgui-quickStart__dot");
+ if (i === this.pageIndex) {
+ addClass(dot, "active");
+ dot.setAttribute("aria-current", "step");
+ }
+ dot.setAttribute("aria-label", `Page ${i + 1}: ${p.title}`);
+ dot.title = p.title;
+ dot.addEventListener("click", () => this.showPage(i));
+ dots.appendChild(dot);
+ });
+ footer.appendChild(dots);
+
+ if (helpUrl) {
+ const guide = document.createElement("a");
+ addClass(guide, "yasgui-quickStart__guide");
+ guide.href = helpUrl;
+ guide.target = "_blank";
+ guide.rel = "noopener noreferrer";
+ guide.innerHTML = `Read the full user guide${icon("externalLink", 14)}`;
+ footer.appendChild(guide);
+ }
+
+ const nav = document.createElement("div");
+ addClass(nav, "yasgui-quickStart__nav");
+ if (this.pageIndex > 0) {
+ const back = document.createElement("button");
+ back.type = "button";
+ addClass(back, "yasgui-quickStart__button", "yasgui-quickStart__back");
+ back.textContent = "Back";
+ back.addEventListener("click", () => this.showPage(this.pageIndex - 1));
+ nav.appendChild(back);
+ } else {
+ const skip = document.createElement("button");
+ skip.type = "button";
+ addClass(skip, "yasgui-quickStart__button", "yasgui-quickStart__skip");
+ skip.textContent = "Skip";
+ skip.addEventListener("click", () => this.close());
+ nav.appendChild(skip);
+ }
+ const next = document.createElement("button");
+ next.type = "button";
+ addClass(next, "yasgui-quickStart__button", "yasgui-quickStart__next", "primary");
+ if (last) {
+ next.textContent = "Start querying";
+ next.addEventListener("click", () => this.close());
+ } else {
+ next.innerHTML = `Next${icon("chevronRight", 16, 2)}`;
+ next.addEventListener("click", () => this.showPage(this.pageIndex + 1));
+ }
+ nav.appendChild(next);
+ footer.appendChild(nav);
+ dialog.appendChild(footer);
+
+ if (focusNav) next.focus();
+ }
+
+ private handleKeydown = (event: KeyboardEvent) => {
+ if (!this.isOpen() || !this.dialogEl) return;
+ if (event.key === "Escape") {
+ event.preventDefault();
+ event.stopPropagation();
+ this.close();
+ return;
+ }
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
+ // Only when no other control in the dialog wants the arrows (the dialog holds buttons and a link)
+ event.preventDefault();
+ event.stopPropagation();
+ this.showPage(this.pageIndex + (event.key === "ArrowRight" ? 1 : -1));
+ return;
+ }
+ if (event.key === "Tab") {
+ // Keep the focus inside the dialog
+ const focusable = Array.from(this.dialogEl.querySelectorAll("button, a[href]"));
+ if (!focusable.length) return;
+ const first = focusable[0];
+ const lastEl = focusable[focusable.length - 1];
+ const active = document.activeElement;
+ if (event.shiftKey && (active === first || active === this.dialogEl)) {
+ event.preventDefault();
+ lastEl.focus();
+ } else if (!event.shiftKey && active === lastEl) {
+ event.preventDefault();
+ first.focus();
+ } else if (!this.dialogEl.contains(active)) {
+ event.preventDefault();
+ first.focus();
+ }
+ }
+ };
+}
diff --git a/packages/yasgui/src/defaults.ts b/packages/yasgui/src/defaults.ts
index 6c10af6b..acb591ed 100644
--- a/packages/yasgui/src/defaults.ts
+++ b/packages/yasgui/src/defaults.ts
@@ -32,6 +32,8 @@ export default function initialize(): Config {
showThemeToggle: true,
showBrand: true,
showCommandPalette: true,
+ showQuickStart: true,
+ quickStartOnFirstVisit: true,
orientation: "vertical",
showSnippetsBar: false,
iriHover: true,
diff --git a/packages/yasgui/src/index.ts b/packages/yasgui/src/index.ts
index 56c53d9c..58b1c1f6 100644
--- a/packages/yasgui/src/index.ts
+++ b/packages/yasgui/src/index.ts
@@ -27,6 +27,7 @@ import SparqlAnythingSupport from "./sparqlAnything/SparqlAnythingSupport";
import SidePanel from "./sidePanel/SidePanel";
import CommandPalette from "./CommandPalette";
import HelpMenu from "./HelpMenu";
+import QuickStart from "./QuickStart";
import WhatsNew from "./whatsNew/WhatsNew";
import AiAssistant from "./ai/AiAssistant";
import type { AiConfig } from "./ai/types";
@@ -120,6 +121,16 @@ export interface Config {
* (default: true)
*/
showCommandPalette?: boolean;
+ /**
+ * "How MatGUI works": a short tour of five pages, in the help menu of the top bar (default: true).
+ * Set to false to leave it out.
+ */
+ showQuickStart?: boolean;
+ /**
+ * Open the quick start tour the first time someone uses MatGUI in this browser (default: true). Only applies
+ * when `showQuickStart` is on. The dismissal is remembered in local storage.
+ */
+ quickStartOnFirstVisit?: boolean;
/**
* Layout orientation: 'vertical' (default) or 'horizontal' (side-by-side)
* 'vertical': YASQE on top, YASR below
@@ -221,6 +232,8 @@ export class Yasgui extends EventEmitter {
public commandPalette: CommandPalette | undefined;
/** Menu of the help button in the top bar; add entries with `helpMenu.addItem()` */
public helpMenu: HelpMenu;
+ /** "How MatGUI works" tour, unless `showQuickStart` is false */
+ public quickStart: QuickStart | undefined;
/** What's new dialog (help menu and Ctrl+K search), unless `showWhatsNew` is false */
public whatsNew: WhatsNew | undefined;
/** AI assistant (the "AI" view of the side panel), unless `ai.enabled` is false */
@@ -261,6 +274,7 @@ export class Yasgui extends EventEmitter {
}
this.helpMenu = new HelpMenu(this);
+ if (this.config.showQuickStart) this.quickStart = new QuickStart(this);
if (this.config.showWhatsNew) this.whatsNew = new WhatsNew(this);
this.tabElements = new TabElements(this);
this.tabPanelsEl = document.createElement("div");
@@ -353,6 +367,14 @@ export class Yasgui extends EventEmitter {
} else {
this.sidePanel.restore();
}
+ if (this.quickStart && this.config.quickStartOnFirstVisit && !QuickStart.hasBeenSeen()) {
+ // After the first paint, so the tour opens over the interface it explains
+ const quickStart = this.quickStart;
+ QuickStart.markSeen();
+ window.setTimeout(() => {
+ if (this.rootEl.isConnected) quickStart.open();
+ }, 300);
+ }
}
public hasFullscreen(fullscreen: boolean) {
if (fullscreen) {
@@ -613,6 +635,7 @@ export class Yasgui extends EventEmitter {
this.sparqlAnything?.destroy();
this.aiAssistant?.destroy();
this.commandPalette?.destroy();
+ this.quickStart?.destroy();
this.whatsNew?.destroy();
this.helpMenu.destroy();
this.sidePanel.destroy();
diff --git a/test/quick-start-browser.ts b/test/quick-start-browser.ts
new file mode 100644
index 00000000..05d12b41
--- /dev/null
+++ b/test/quick-start-browser.ts
@@ -0,0 +1,150 @@
+import * as path from "path";
+import * as http from "http";
+import * as chai from "chai";
+import * as puppeteer from "puppeteer";
+import { it, describe, before, beforeEach, after, afterEach } from "mocha";
+import { setup, destroy, closePage, getPage, wait } from "./utils.js";
+
+const expect = chai.expect;
+
+describe("Quick start tour", function () {
+ let browser: puppeteer.Browser;
+ let page: puppeteer.Page;
+ let server: http.Server | undefined;
+
+ before(async function () {
+ const refs = await setup(this, path.resolve("./build"));
+ browser = refs.browser;
+ server = refs.server;
+ });
+
+ beforeEach(async function () {
+ this.timeout(30000);
+ page = await getPage(browser, "yasgui.html", { quickStart: true });
+ await page.evaluate(() => localStorage.clear());
+ await page.reload({ waitUntil: "networkidle2" });
+ await page.waitForFunction(() => "yasgui" in window);
+ });
+
+ afterEach(async () => {
+ await closePage(this, page);
+ });
+
+ after(async function () {
+ return destroy(browser, server);
+ });
+
+ const visibleTour = () =>
+ page.evaluate(() => {
+ const overlay = document.querySelector(".yasgui-quickStart");
+ return !!overlay && !overlay.hidden;
+ });
+ const currentPage = () =>
+ page.evaluate(() => ({
+ title: document.querySelector(".yasgui-quickStart__title")?.textContent,
+ eyebrow: document.querySelector(".yasgui-quickStart__eyebrow")?.textContent,
+ items: document.querySelectorAll(".yasgui-quickStart__item").length,
+ illustrations: document.querySelectorAll(".yasgui-quickStart__illustration svg").length,
+ }));
+
+ it("opens on the first visit and walks through five pages of three items", async function () {
+ await page.waitForSelector(".yasgui-quickStart:not([hidden])", { visible: true });
+ const titles: string[] = [];
+ for (let i = 0; i < 5; i++) {
+ const state = await currentPage();
+ expect(state.eyebrow).to.equal(`How MatGUI works · ${i + 1} of 5`);
+ expect(state.items).to.equal(3);
+ expect(state.illustrations).to.equal(3);
+ titles.push(state.title as string);
+ await page.click(".yasgui-quickStart__next");
+ }
+ expect(titles[0]).to.equal("Write and run a query");
+ expect(new Set(titles).size).to.equal(5);
+ // "Start querying" on the last page closes the tour
+ expect(await visibleTour()).to.equal(false);
+ });
+
+ it("goes back, jumps with the dots and the arrow keys, and closes with Escape", async function () {
+ await page.waitForSelector(".yasgui-quickStart:not([hidden])", { visible: true });
+ await page.click(".yasgui-quickStart__next");
+ await page.click(".yasgui-quickStart__back");
+ expect((await currentPage()).eyebrow).to.contain("1 of 5");
+ await page.click(".yasgui-quickStart__dot:nth-child(4)");
+ expect((await currentPage()).eyebrow).to.contain("4 of 5");
+ await page.keyboard.press("ArrowRight");
+ expect((await currentPage()).eyebrow).to.contain("5 of 5");
+ await page.keyboard.press("ArrowLeft");
+ expect((await currentPage()).eyebrow).to.contain("4 of 5");
+ await page.keyboard.press("Escape");
+ expect(await visibleTour()).to.equal(false);
+ });
+
+ it("links to the user guide", async function () {
+ await page.waitForSelector(".yasgui-quickStart:not([hidden])", { visible: true });
+ const link = await page.evaluate(() => {
+ const a = document.querySelector(".yasgui-quickStart__guide");
+ return { href: a?.href, target: a?.target, helpUrl: (window as any).yasgui.config.helpUrl };
+ });
+ expect(link.href).to.equal(link.helpUrl);
+ expect(link.target).to.equal("_blank");
+ });
+
+ it("does not open again once seen, but stays in the help menu", async function () {
+ await page.waitForSelector(".yasgui-quickStart:not([hidden])", { visible: true });
+ await page.click(".yasgui-quickStart__close");
+ await page.reload({ waitUntil: "networkidle2" });
+ await wait(500);
+ expect(await visibleTour()).to.equal(false);
+
+ await page.click(".yasgui-helpButton");
+ const labels = await page.$$eval(".yasgui-helpMenu__item", (items) => items.map((i) => i.textContent?.trim()));
+ expect(labels[0]).to.equal("How MatGUI works");
+ expect(labels[labels.length - 1]).to.equal("Documentation");
+ await page.click(".yasgui-helpMenu__item");
+ await page.waitForSelector(".yasgui-quickStart:not([hidden])", { visible: true });
+ expect((await currentPage()).eyebrow).to.contain("1 of 5");
+ });
+
+ it("can be left out with showQuickStart: false", async function () {
+ await page.evaluate(() => {
+ localStorage.clear();
+ const Yasgui = (window as any).Yasgui;
+ const div = document.createElement("div");
+ document.body.appendChild(div);
+ (window as any).other = new Yasgui(div, { showQuickStart: false, persistenceId: "noQuickStart" });
+ });
+ await wait(600);
+ const result = await page.evaluate(() => {
+ const other = (window as any).other;
+ return {
+ tour: !!other.rootEl.querySelector(".yasgui-quickStart"),
+ quickStart: other.quickStart,
+ items: other.helpMenu.getItems().map((item: any) => item.id),
+ };
+ });
+ expect(result.tour).to.equal(false);
+ expect(result.quickStart).to.equal(undefined);
+ expect(result.items).to.not.include("quickStart");
+ });
+
+ it("can stay in the help menu without opening on the first visit", async function () {
+ await page.evaluate(() => {
+ localStorage.clear();
+ const Yasgui = (window as any).Yasgui;
+ const div = document.createElement("div");
+ document.body.appendChild(div);
+ (window as any).other = new Yasgui(div, { quickStartOnFirstVisit: false, persistenceId: "manualQuickStart" });
+ });
+ await wait(600);
+ const result = await page.evaluate(() => {
+ const other = (window as any).other;
+ const overlay = other.rootEl.querySelector(".yasgui-quickStart");
+ return {
+ open: !!overlay && !overlay.hidden,
+ items: other.helpMenu.getItems().map((item: any) => item.id),
+ };
+ });
+ expect(result.open).to.equal(false);
+ expect(result.items[0]).to.equal("quickStart");
+ });
+});
diff --git a/test/run.ts b/test/run.ts
index e06b6de6..c0875b86 100644
--- a/test/run.ts
+++ b/test/run.ts
@@ -11,6 +11,7 @@ import Yasqe from "@matdata/yasqe";
import { setup, destroy, closePage, getPage, makeScreenshot, inspectLive, wait } from "./utils.js";
import "./endpoint-describe-browser.js";
import "./iri-hover-browser.js";
+import "./quick-start-browser.js";
import "./ai-assistant-browser.js";
import "./sparql-anything-browser.js";
import "./whats-new-browser.js";
diff --git a/test/utils.ts b/test/utils.ts
index 513b392d..308080e4 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -85,8 +85,18 @@ export async function destroy(browser: Browser, server?: http.Server) {
if (browser) await browser.close();
if (server) server.close();
}
-export async function getPage(browser: Browser, path: string) {
+export async function getPage(browser: Browser, path: string, options: { quickStart?: boolean } = {}) {
const page = await browser.newPage();
+ // The quick start tour opens on the first visit and would cover the page; tests that want it pass quickStart
+ if (!options.quickStart) {
+ await page.evaluateOnNewDocument(() => {
+ try {
+ localStorage.setItem("matgui_quickStart_seen", "1");
+ } catch {
+ // ignore
+ }
+ });
+ }
await page.goto(`http://localhost:${PORT}/${path}?noExternalServices=true`, {
waitUntil: "networkidle2",
timeout: 30000,