Add the tour logic, introduction and homepage tours.
This commit is contained in:
		
							parent
							
								
									8c62714b9b
								
							
						
					
					
						commit
						97cb3a5603
					
				| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					import {homepageSteps, homepageEventHandlers} from "./interface/exports.js";
 | 
				
			||||||
 | 
					import {introductionSteps} from "./introduction.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const tourIds = ["introduction", "interface-homepage"] as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const tourIdsAndSteps: Array<
 | 
				
			||||||
 | 
					  [TourId, TourStepOptions[], TourStepEventHandler[]]
 | 
				
			||||||
 | 
					> = [
 | 
				
			||||||
 | 
					  ["introduction", introductionSteps, []],
 | 
				
			||||||
 | 
					  ["interface-homepage", homepageSteps, homepageEventHandlers],
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  eventHandlers as homepageEventHandlers,
 | 
				
			||||||
 | 
					  steps as homepageSteps,
 | 
				
			||||||
 | 
					} from "./homepage.js";
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,499 @@
 | 
				
			||||||
 | 
					import type Shepherd from "shepherd.js";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  addDatasetCounter,
 | 
				
			||||||
 | 
					  encapsulateElements,
 | 
				
			||||||
 | 
					  removeAllDatasetCounters,
 | 
				
			||||||
 | 
					  renderInContainer,
 | 
				
			||||||
 | 
					} from "../utilities.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const step01 = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>The Homepage</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      If you plan on staying a while, this is likely the place you'll see the
 | 
				
			||||||
 | 
					      most. So let's work our way top to bottom, left to right.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>Starting with...</p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const step02 = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>The Main Header</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      On the left there is the Tildes logo and title, which you can click to get
 | 
				
			||||||
 | 
					      back to the homepage, or refresh it if you're already there. If you are in
 | 
				
			||||||
 | 
					      a group, the group name will also be shown.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      On the right is the notifications section and your username. When you have
 | 
				
			||||||
 | 
					      unread messages and/or are mentioned, they will show up next to your
 | 
				
			||||||
 | 
					      username. If you don't have any notifications right now, you can{" "}
 | 
				
			||||||
 | 
					      <a href="#" onClick={toggleMockNotifications}>
 | 
				
			||||||
 | 
					        click here
 | 
				
			||||||
 | 
					      </a>{" "}
 | 
				
			||||||
 | 
					      to show a preview of what they look like.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      At the moment mentions only work in comments, so if you get mentioned in a
 | 
				
			||||||
 | 
					      topic's text body and don't get a notification,{" "}
 | 
				
			||||||
 | 
					      <a target="_blank" href="https://gitlab.com/tildes/tildes/-/issues/195">
 | 
				
			||||||
 | 
					        that's why
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					      .
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Toggle mock notifications, based on the code in the following link.
 | 
				
			||||||
 | 
					// If the logic for this needs to be updated, also update the permalink to the
 | 
				
			||||||
 | 
					// actual Tildes HTML code.
 | 
				
			||||||
 | 
					// https://gitlab.com/tildes/tildes/-/blob/0dbb031562cd9297968fec0049af4b833ef77301/tildes/tildes/templates/macros/user.jinja2#L9-20
 | 
				
			||||||
 | 
					function toggleMockNotifications(event: Event): void {
 | 
				
			||||||
 | 
					  // Prevent the click from going through so the anchor isn't removed.
 | 
				
			||||||
 | 
					  event.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const root = document.querySelector("#site-header .logged-in-user-info");
 | 
				
			||||||
 | 
					  function toggle(existingSelector: string, href: string, text: string): void {
 | 
				
			||||||
 | 
					    const existing =
 | 
				
			||||||
 | 
					      root?.querySelector<HTMLElement>(existingSelector) ?? undefined;
 | 
				
			||||||
 | 
					    if (existing === undefined) {
 | 
				
			||||||
 | 
					      // If no notifiaction exists, render our mock.
 | 
				
			||||||
 | 
					      const messageNotification = document.createElement("a");
 | 
				
			||||||
 | 
					      const count = 1 + Math.ceil(Math.random() * 10);
 | 
				
			||||||
 | 
					      messageNotification.classList.add("logged-in-user-alert");
 | 
				
			||||||
 | 
					      messageNotification.dataset.tildesShepherdMock = "true";
 | 
				
			||||||
 | 
					      messageNotification.href = href;
 | 
				
			||||||
 | 
					      messageNotification.textContent = `${count} ${text}`;
 | 
				
			||||||
 | 
					      root?.insertAdjacentElement("beforeend", messageNotification);
 | 
				
			||||||
 | 
					    } else if (existing.dataset.tildesShepherdMock === "true") {
 | 
				
			||||||
 | 
					      // If a notification exists and it has our mock attribute, remove it.
 | 
				
			||||||
 | 
					      existing.remove();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // A real message notification already exists, so we do nothing.
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toggle('[href="/messages/unread"]', "/messages/unread", "new messages");
 | 
				
			||||||
 | 
					  toggle(
 | 
				
			||||||
 | 
					    '[href="/notifications/unread"]',
 | 
				
			||||||
 | 
					    "/notifications/unread",
 | 
				
			||||||
 | 
					    "new comments",
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const step03 = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>The Listing Options</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Right below the main header are the topic listing options. These determine
 | 
				
			||||||
 | 
					      how the topics in the listing are sorted and{" "}
 | 
				
			||||||
 | 
					      <em>
 | 
				
			||||||
 | 
					        <b>from</b> what period
 | 
				
			||||||
 | 
					      </em>{" "}
 | 
				
			||||||
 | 
					      topics should be shown.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      The non-activity sorts are the easiest to explain, so let's start with
 | 
				
			||||||
 | 
					      them:
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ul>
 | 
				
			||||||
 | 
					      <li>
 | 
				
			||||||
 | 
					        <b>Votes</b> sorts topics with the most votes first.
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					      <li>
 | 
				
			||||||
 | 
					        <b>Comments</b> sorts them with the most comments first.
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					      <li>
 | 
				
			||||||
 | 
					        And <b>New</b> sorts them by their date, so newest topics first.
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					    </ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Then the first <b>Activity</b> will sort topics by bumping topics up as
 | 
				
			||||||
 | 
					      they receive comments, however this sort makes use of comment labels.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      If you don't know what comment labels are yet, don't worry, there is a
 | 
				
			||||||
 | 
					      tour that explains them. But in short they are a community tool to
 | 
				
			||||||
 | 
					      emphasize and de-emphasize comments, similar to votes but with more
 | 
				
			||||||
 | 
					      specific intention. If you'd like to read more, check out{" "}
 | 
				
			||||||
 | 
					      <a
 | 
				
			||||||
 | 
					        target="_blank"
 | 
				
			||||||
 | 
					        href="https://docs.tildes.net/instructions/commenting-on-tildes#labelling-comments"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        the documentation
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					      .
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      And finally the <b>All activity</b> sorts topics the same as Activity, but
 | 
				
			||||||
 | 
					      without taking into account any comment labels.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      If you only want to see topics from a certain period, click on the "from"
 | 
				
			||||||
 | 
					      dropdown and select your period. Aside from a set of predefined options,
 | 
				
			||||||
 | 
					      you can also set a custom one by clicking the "other period" option, after
 | 
				
			||||||
 | 
					      which you'll be prompted for it.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const step04 = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>The Topic Listing</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Next up, just below the listing options is the topic listing itself. We've
 | 
				
			||||||
 | 
					      only highlighted the first topic here, but you can probably see it
 | 
				
			||||||
 | 
					      continues all the way down the page.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>Here, we've marked the main elements of the topic:</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ol>
 | 
				
			||||||
 | 
					      <li>
 | 
				
			||||||
 | 
					        The topic title, when the topic is a "text topic" links to the same
 | 
				
			||||||
 | 
					        place as the comments link does. Otherwise when it's a "link topic", it
 | 
				
			||||||
 | 
					        will link to an external site.
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <li>
 | 
				
			||||||
 | 
					        The topic metadata, which by default includes the group it's in. If a
 | 
				
			||||||
 | 
					        topic has specific notable tags and you have the "show tags" setting
 | 
				
			||||||
 | 
					        enabled in your user settings, they will be shown after the group name.
 | 
				
			||||||
 | 
					        A "topic type" may also be shown to indicate whether the topic is a text
 | 
				
			||||||
 | 
					        topic, a video, an "ask" topic for recommendations or a survey, etc.
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <li>
 | 
				
			||||||
 | 
					        The amount of comments the topic has received. If any new comments have
 | 
				
			||||||
 | 
					        been posted since you last viewed the topic, in orange a "(X new)" will
 | 
				
			||||||
 | 
					        be added.
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <li>
 | 
				
			||||||
 | 
					        The topic source. For text topics this is always the poster of the
 | 
				
			||||||
 | 
					        topic, but for link topics in certain groups it will be the domain name
 | 
				
			||||||
 | 
					        together with that website's icon. For topics posted automatically by
 | 
				
			||||||
 | 
					        Tildes itself, it will be "Scheduled topic".
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					    </ol>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>The list continues on the next page.</p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const step05 = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>The Topic Listing continued</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ol start={5}>
 | 
				
			||||||
 | 
					      <li>
 | 
				
			||||||
 | 
					        The date when the topic was posted. For dates that are pretty recent it
 | 
				
			||||||
 | 
					        will show something like "X days ago" while for longer dates it will be
 | 
				
			||||||
 | 
					        the exact date, like "April 26th, 2021".
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <li>
 | 
				
			||||||
 | 
					        The voting button, clicking it will add your vote to the topic. On
 | 
				
			||||||
 | 
					        Tildes, once the topic is older than 30 days you can no longer vote on
 | 
				
			||||||
 | 
					        it and the individual voting data for is removed, with only the total
 | 
				
			||||||
 | 
					        count kept. You can read more about it in{" "}
 | 
				
			||||||
 | 
					        <a target="_blank" href="https://tild.es/jhm">
 | 
				
			||||||
 | 
					          this announcement topic
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					        .
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <li>
 | 
				
			||||||
 | 
					        And finally the topic actions. Clicking on the "Actions" button will
 | 
				
			||||||
 | 
					        show a dropdown including "Bookmark", "Ignore this topic" and if you
 | 
				
			||||||
 | 
					        have been granted the permission for it, "Edit title". Ignoring the
 | 
				
			||||||
 | 
					        topic will remove it from the topic listing and prevent any future
 | 
				
			||||||
 | 
					        notifications from happening. Both your bookmarked and ignored topics
 | 
				
			||||||
 | 
					        can be found in their respective sections on your profile.
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					    </ol>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const step06 = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>The Sidebar</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Let's take a look at the sidebar, where the first thing we're greeted with
 | 
				
			||||||
 | 
					      is the search bar.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      A quick note for mobile use, the sidebar can opened via a button that
 | 
				
			||||||
 | 
					      appears in the very top-right of the page.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      When searching from the homepage, topics will be included from any group.
 | 
				
			||||||
 | 
					      However, searching when inside a group will only include topics from that
 | 
				
			||||||
 | 
					      group and any sub-groups.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Below the search bar you will also find the title of the page, or name of
 | 
				
			||||||
 | 
					      the group, and a small description. The sidebar has more elements when in
 | 
				
			||||||
 | 
					      you're in a group page, but those are touched upon in the groups tour.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const step07 = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>The Sidebar Subscriptions</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Moving on, you'll find the list of groups you are subscribed to and a
 | 
				
			||||||
 | 
					      button below it to go to the groups listing, where you can find all the
 | 
				
			||||||
 | 
					      that are available.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      To post a topic, subscribe or unsubscribe from any group, go to the group
 | 
				
			||||||
 | 
					      in question and in the sidebar there will be buttons to do so.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const step08 = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>The Sidebar User Settings</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      At the bottom of the sidebar you will find some user settings.
 | 
				
			||||||
 | 
					      Specifically the filtered topics tags you have set and a link to the
 | 
				
			||||||
 | 
					      settings page.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      When you have filtered topics tags set, any topics with any of the tags
 | 
				
			||||||
 | 
					      present will be removed from your listing. You can read more about it by
 | 
				
			||||||
 | 
					      going to the dedicated page for it by clicking the "Edit filtered tags"
 | 
				
			||||||
 | 
					      button.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const step09 = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>The Footer</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      And finally, the very last part of the homepage is the footer. Here you
 | 
				
			||||||
 | 
					      will find a theme selector (more on that in a moment) and various links to
 | 
				
			||||||
 | 
					      places, such as the Tildes Documentation, Contact page, the source code,
 | 
				
			||||||
 | 
					      etc. We highly recommend reading through the documentation as it explains
 | 
				
			||||||
 | 
					      a lot of the how and why Tildes does certain things.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      As for the theme selector, you can change your theme with a number of
 | 
				
			||||||
 | 
					      options. When you change it here, it will only change for the current
 | 
				
			||||||
 | 
					      device, if you'd like to set an account default for all devices, head to
 | 
				
			||||||
 | 
					      your account settings.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const step10 = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>This is the end, beautiful friend</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>And that's the end of the Homepage tour!</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      As always, if you find any bugs, have feature requests or simply want to
 | 
				
			||||||
 | 
					      ask a question. Please send us a message at{" "}
 | 
				
			||||||
 | 
					      <a target="_blank" href="https://tildes.net/user/Community">
 | 
				
			||||||
 | 
					        @Community
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					      .
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>Happy Tildying!~</p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const steps: Shepherd.Step.StepOptions[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: "homepage-01",
 | 
				
			||||||
 | 
					    text: step01,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    attachTo: {
 | 
				
			||||||
 | 
					      element: "#site-header",
 | 
				
			||||||
 | 
					      on: "bottom",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canClickTarget: false,
 | 
				
			||||||
 | 
					    id: "homepage-02",
 | 
				
			||||||
 | 
					    scrollTo: true,
 | 
				
			||||||
 | 
					    text: step02,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    attachTo: {
 | 
				
			||||||
 | 
					      element: "main > .listing-options",
 | 
				
			||||||
 | 
					      on: "bottom",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canClickTarget: false,
 | 
				
			||||||
 | 
					    id: "homepage-03",
 | 
				
			||||||
 | 
					    text: step03,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    attachTo: {
 | 
				
			||||||
 | 
					      element: ".topic-listing > li:first-child",
 | 
				
			||||||
 | 
					      on: "bottom",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canClickTarget: false,
 | 
				
			||||||
 | 
					    id: "homepage-04",
 | 
				
			||||||
 | 
					    text: step04,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    attachTo: {
 | 
				
			||||||
 | 
					      element: ".topic-listing > li:first-child",
 | 
				
			||||||
 | 
					      on: "bottom",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canClickTarget: false,
 | 
				
			||||||
 | 
					    id: "homepage-05",
 | 
				
			||||||
 | 
					    text: step05,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    attachTo: {
 | 
				
			||||||
 | 
					      element: '#sidebar [data-tildes-shepherd-encapsulated="homepage-06"]',
 | 
				
			||||||
 | 
					      on: "left-start",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canClickTarget: false,
 | 
				
			||||||
 | 
					    id: "homepage-06",
 | 
				
			||||||
 | 
					    text: step06,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    attachTo: {
 | 
				
			||||||
 | 
					      element: '#sidebar [data-tildes-shepherd-encapsulated="homepage-07"]',
 | 
				
			||||||
 | 
					      on: "left-start",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canClickTarget: false,
 | 
				
			||||||
 | 
					    id: "homepage-07",
 | 
				
			||||||
 | 
					    scrollTo: true,
 | 
				
			||||||
 | 
					    text: step07,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    attachTo: {
 | 
				
			||||||
 | 
					      element: "#sidebar .divider + .nav",
 | 
				
			||||||
 | 
					      on: "left-start",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canClickTarget: false,
 | 
				
			||||||
 | 
					    id: "homepage-08",
 | 
				
			||||||
 | 
					    scrollTo: true,
 | 
				
			||||||
 | 
					    text: step08,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    attachTo: {
 | 
				
			||||||
 | 
					      element: "#site-footer",
 | 
				
			||||||
 | 
					      on: "top",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canClickTarget: false,
 | 
				
			||||||
 | 
					    id: "homepage-09",
 | 
				
			||||||
 | 
					    scrollTo: true,
 | 
				
			||||||
 | 
					    text: step09,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    canClickTarget: false,
 | 
				
			||||||
 | 
					    id: "homepage-10",
 | 
				
			||||||
 | 
					    text: step10,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const eventHandlers: TourStepEventHandler[] = [
 | 
				
			||||||
 | 
					  [
 | 
				
			||||||
 | 
					    "homepage-04",
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      "show",
 | 
				
			||||||
 | 
					      () => {
 | 
				
			||||||
 | 
					        const topic = ".topic-listing > li:first-child";
 | 
				
			||||||
 | 
					        const counters = [
 | 
				
			||||||
 | 
					          ".topic-title",
 | 
				
			||||||
 | 
					          ".topic-metadata",
 | 
				
			||||||
 | 
					          ".topic-info-comments",
 | 
				
			||||||
 | 
					          ".topic-info-source",
 | 
				
			||||||
 | 
					          "time",
 | 
				
			||||||
 | 
					          ".topic-voting",
 | 
				
			||||||
 | 
					          ".topic-actions",
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const [count, selector] of counters.entries()) {
 | 
				
			||||||
 | 
					          addDatasetCounter(`${topic} ${selector}`, count + 1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  [
 | 
				
			||||||
 | 
					    "homepage-05",
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      "destroy",
 | 
				
			||||||
 | 
					      () => {
 | 
				
			||||||
 | 
					        removeAllDatasetCounters();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  [
 | 
				
			||||||
 | 
					    "homepage-05",
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      "show",
 | 
				
			||||||
 | 
					      () => {
 | 
				
			||||||
 | 
					        encapsulateElements(
 | 
				
			||||||
 | 
					          "homepage-06",
 | 
				
			||||||
 | 
					          "#sidebar .sidebar-controls",
 | 
				
			||||||
 | 
					          "afterend",
 | 
				
			||||||
 | 
					          ["#sidebar .form-search", "#sidebar h2", "#sidebar p"],
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  [
 | 
				
			||||||
 | 
					    "homepage-06",
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      "show",
 | 
				
			||||||
 | 
					      () => {
 | 
				
			||||||
 | 
					        encapsulateElements("homepage-07", "#sidebar .divider", "beforebegin", [
 | 
				
			||||||
 | 
					          "#sidebar .nav",
 | 
				
			||||||
 | 
					          '#sidebar [href="/groups"',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  [
 | 
				
			||||||
 | 
					    "homepage-08",
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      "show",
 | 
				
			||||||
 | 
					      () => {
 | 
				
			||||||
 | 
					        const filteredTags =
 | 
				
			||||||
 | 
					          document.querySelector<HTMLDetailsElement>("#sidebar details") ??
 | 
				
			||||||
 | 
					          undefined;
 | 
				
			||||||
 | 
					        if (filteredTags === undefined) {
 | 
				
			||||||
 | 
					          console.warn("Element is unexpectedly undefined");
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        filteredTags.open = true;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,122 @@
 | 
				
			||||||
 | 
					import type Shepherd from "shepherd.js";
 | 
				
			||||||
 | 
					import {createIntroductionUnderstood} from "../storage/common.js";
 | 
				
			||||||
 | 
					import {openOptionsPageFromBackground, renderInContainer} from "./utilities.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const stepOne = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>Thank you for installing Tildes Shepherd!</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Tildes Shepherd is a set of interactive guided tours for Tildes to show
 | 
				
			||||||
 | 
					      you around and introduce you to the concepts that make this website great.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      To see and start the tours that are available, click on the extension icon
 | 
				
			||||||
 | 
					      to go to the options page or{" "}
 | 
				
			||||||
 | 
					      <a href="#" onClick={openOptionsPageFromBackground}>
 | 
				
			||||||
 | 
					        click here now
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					      .
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Each tour will start with a pop-up like this one and have "Continue",
 | 
				
			||||||
 | 
					      "Back" and "Exit" buttons at the bottom. They will progress the tour
 | 
				
			||||||
 | 
					      forward, backward or exit it.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const stepTwo = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <h1>Tour Mechanics</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      During the tours, at various points we will want to highlight specific
 | 
				
			||||||
 | 
					      areas. When that happens you won't be able to click anything inside them,
 | 
				
			||||||
 | 
					      mainly to prevent you from accidentally going to a different page and
 | 
				
			||||||
 | 
					      interrupting the tour. But also to prevent you from taking actions you
 | 
				
			||||||
 | 
					      maybe don't want to take, like voting on something you don't necessarily
 | 
				
			||||||
 | 
					      want to vote on.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Depending on your selected theme, the highlighted areas will have a yellow
 | 
				
			||||||
 | 
					      or orange border and some extra added whitespace. As you can see in this
 | 
				
			||||||
 | 
					      example where the main site header has been highlighted.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const stepThree = renderInContainer(
 | 
				
			||||||
 | 
					  <>
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      If you find any bugs, have feature requests or simply want to ask a
 | 
				
			||||||
 | 
					      question. Please send us a message at{" "}
 | 
				
			||||||
 | 
					      <a target="_blank" href="https://tildes.net/user/Community">
 | 
				
			||||||
 | 
					        @Community
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					      .
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Also, big shoutout to the people at{" "}
 | 
				
			||||||
 | 
					      <a target="_blank" href="https://shipshape.io">
 | 
				
			||||||
 | 
					        Ship Shape
 | 
				
			||||||
 | 
					      </a>{" "}
 | 
				
			||||||
 | 
					      for making{" "}
 | 
				
			||||||
 | 
					      <a target="_blank" href="https://shepherdjs.dev">
 | 
				
			||||||
 | 
					        Shepherd.js
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					      , the software library making this entire project so much easier to
 | 
				
			||||||
 | 
					      create. And the namesake of it, too.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Once you click the "I understand" button below, this message won't pop up
 | 
				
			||||||
 | 
					      again, so remember the extension icon is how you get to the tours.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>Happy Tildying!~</p>
 | 
				
			||||||
 | 
					  </>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const introductionSteps: Shepherd.Step.StepOptions[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: "introduction-1",
 | 
				
			||||||
 | 
					    text: stepOne,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    attachTo: {
 | 
				
			||||||
 | 
					      element: "#site-header",
 | 
				
			||||||
 | 
					      on: "bottom",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canClickTarget: false,
 | 
				
			||||||
 | 
					    id: "introduction-2",
 | 
				
			||||||
 | 
					    text: stepTwo,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    buttons: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        classes: "btn",
 | 
				
			||||||
 | 
					        text: "I understand",
 | 
				
			||||||
 | 
					        async action() {
 | 
				
			||||||
 | 
					          const introductionUnderstood = await createIntroductionUnderstood();
 | 
				
			||||||
 | 
					          introductionUnderstood.value = true;
 | 
				
			||||||
 | 
					          await introductionUnderstood.save();
 | 
				
			||||||
 | 
					          this.complete();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        classes: "btn",
 | 
				
			||||||
 | 
					        text: "Back",
 | 
				
			||||||
 | 
					        action() {
 | 
				
			||||||
 | 
					          this.back();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    id: "introduction-3",
 | 
				
			||||||
 | 
					    text: stepThree,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,106 @@
 | 
				
			||||||
 | 
					import {type JSX, render} from "preact";
 | 
				
			||||||
 | 
					import browser from "webextension-polyfill";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Adds a `[data-tildes-shepherd-counter]` attribute to a specified element. For
 | 
				
			||||||
 | 
					 * the associated CSS, see `source/content-scripts/scss/main.scss`.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param selector The selector of element to apply the counter to, if the
 | 
				
			||||||
 | 
					 * target element can't be selected an error will be thrown.
 | 
				
			||||||
 | 
					 * @param count The number to display in the counter.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function addDatasetCounter(selector: string, count: number) {
 | 
				
			||||||
 | 
					  const element = document.querySelector<HTMLElement>(selector) ?? undefined;
 | 
				
			||||||
 | 
					  if (element === undefined) {
 | 
				
			||||||
 | 
					    throw new Error(`Target element for "${selector}" is undefined`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  element.dataset.tildesShepherdCounter = count.toString();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Create a new `div` element and put all the specified elements inside it. Used
 | 
				
			||||||
 | 
					 * for highlighting groups of elements that have sibling elements we may not
 | 
				
			||||||
 | 
					 * want to highlight. Like highlighting only the search bar, title and
 | 
				
			||||||
 | 
					 * description in the homepage sidebar, but not all the rest of the sidebar.
 | 
				
			||||||
 | 
					 * @param stepId The unique step ID to set as
 | 
				
			||||||
 | 
					 * `data-tildes-shepherd-encapsulated="<id>"`. Used for attaching the Shepherd
 | 
				
			||||||
 | 
					 * step to the container.
 | 
				
			||||||
 | 
					 * @param rootSelector The selector for the root element to insert the container
 | 
				
			||||||
 | 
					 * near. This doesn't need to be the parent element necessarily, since
 | 
				
			||||||
 | 
					 * {@link Element.insertAdjacentElement} is used to do the inserting it can also
 | 
				
			||||||
 | 
					 * be inserted before or after the root element.
 | 
				
			||||||
 | 
					 * @param position The insert position for {@link Element.insertAdjacentElement}.
 | 
				
			||||||
 | 
					 * @param selectors The list of element selectors to include in the new
 | 
				
			||||||
 | 
					 * container. Note that you should make sure any elements included don't break
 | 
				
			||||||
 | 
					 * their CSS when contained by a new `div` element and ideally they should all
 | 
				
			||||||
 | 
					 * be adjacent to one another and specified in the order they appear in.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function encapsulateElements(
 | 
				
			||||||
 | 
					  stepId: string,
 | 
				
			||||||
 | 
					  rootSelector: string,
 | 
				
			||||||
 | 
					  position: InsertPosition,
 | 
				
			||||||
 | 
					  selectors: string[],
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const container = document.createElement("div");
 | 
				
			||||||
 | 
					  container.dataset.tildesShepherdEncapsulated = stepId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const selector of selectors) {
 | 
				
			||||||
 | 
					    const element = document.querySelector<HTMLElement>(selector) ?? undefined;
 | 
				
			||||||
 | 
					    if (element === undefined) {
 | 
				
			||||||
 | 
					      console.warn(`Unexpected undefined element: ${selector}`);
 | 
				
			||||||
 | 
					      continue;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Don't encapsulate anything if the parent is already one of our elements.
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      element.parentElement?.dataset.tildesShepherdEncapsulated !== undefined
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    container.append(element);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const root = document.querySelector(rootSelector) ?? undefined;
 | 
				
			||||||
 | 
					  if (root === undefined) {
 | 
				
			||||||
 | 
					    throw new Error(`Root selector returned undefined: ${rootSelector}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  root.insertAdjacentElement(position, container);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Content scripts can't open the options page themselves, so send a message to
 | 
				
			||||||
 | 
					 * the background script and open it there.
 | 
				
			||||||
 | 
					 * @param event The mouse click event to prevent from happening.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export async function openOptionsPageFromBackground(
 | 
				
			||||||
 | 
					  event: MouseEvent,
 | 
				
			||||||
 | 
					): Promise<void> {
 | 
				
			||||||
 | 
					  event.preventDefault();
 | 
				
			||||||
 | 
					  await browser.runtime.sendMessage("open-options-page");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Removes all elements with a `data-tildes-shepherd-counter` set.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function removeAllDatasetCounters() {
 | 
				
			||||||
 | 
					  const elements = document.querySelectorAll<HTMLElement>(
 | 
				
			||||||
 | 
					    "[data-tildes-shepherd-counter]",
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  for (const element of Array.from(elements)) {
 | 
				
			||||||
 | 
					    delete element.dataset.tildesShepherdCounter;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Render JSX inside a `div` and return the div. This is mostly used so we can
 | 
				
			||||||
 | 
					 * pass JSX more easily to Shepherd, which doesn't accept JSX directly but does
 | 
				
			||||||
 | 
					 * accept {@link HTMLElement}. So this bit of indirection gets us there.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function renderInContainer(root: JSX.Element): HTMLElement {
 | 
				
			||||||
 | 
					  const container = document.createElement("div");
 | 
				
			||||||
 | 
					  render(root, container);
 | 
				
			||||||
 | 
					  return container;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,14 @@
 | 
				
			||||||
// Export something so TypeScript doesn't see this file as an ambient module.
 | 
					import type Shepherd from "shepherd.js";
 | 
				
			||||||
export {};
 | 
					import type {tourIds} from "./tours/exports.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
  const $browser: "chromium" | "firefox";
 | 
					  const $browser: "chromium" | "firefox";
 | 
				
			||||||
  const $dev: boolean;
 | 
					  const $dev: boolean;
 | 
				
			||||||
  const $test: boolean;
 | 
					  const $test: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  type TourId = (typeof tourIds)[number];
 | 
				
			||||||
 | 
					  type TourStepEvent = "show" | "destroy";
 | 
				
			||||||
 | 
					  type TourStepEventFunction = Parameters<Shepherd.Step["on"]>[1];
 | 
				
			||||||
 | 
					  type TourStepEventHandler = [string, [TourStepEvent, TourStepEventFunction]];
 | 
				
			||||||
 | 
					  type TourStepOptions = Shepherd.Step.StepOptions;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue