diff --git a/source/options/app.tsx b/source/options/app.tsx
new file mode 100644
index 0000000..53eb4c8
--- /dev/null
+++ b/source/options/app.tsx
@@ -0,0 +1,14 @@
+import {Component} from "preact";
+import {PageHeader} from "./components/page-header.js";
+import {Tours} from "./components/tours.js";
+
+export class App extends Component {
+ render() {
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/source/options/components/page-header.tsx b/source/options/components/page-header.tsx
new file mode 100644
index 0000000..bead87e
--- /dev/null
+++ b/source/options/components/page-header.tsx
@@ -0,0 +1,12 @@
+import {Component} from "preact";
+
+export class PageHeader extends Component {
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/source/options/components/tour.tsx b/source/options/components/tour.tsx
new file mode 100644
index 0000000..1813e8e
--- /dev/null
+++ b/source/options/components/tour.tsx
@@ -0,0 +1,74 @@
+import {Component, type JSX} from "preact";
+
+type Props = {
+ hasBeenCompleted: boolean;
+ name: string;
+ tourId: TourId;
+};
+
+function tourDescription(tourId: Props["tourId"]): JSX.Element {
+ if (tourId === "introduction") {
+ return (
+
+ A short introduction to Tildes Shepherd and how the tours work. Normally
+ this is automatically shown when you first installed the extension.
+
+ );
+ }
+
+ if (tourId === "interface-homepage") {
+ return (
+
+ Let's take a look at the home page and all we can do there.
+
+ );
+ }
+
+ return (
+
+ Tour ID "{tourId}" does not have a description, this should probably be
+ fixed!
+
+ );
+}
+
+function tourLink(tourId: Props["tourId"]): string {
+ const anchor = `#tildes-shepherd-tour=${tourId}`;
+ const baseUrl = "https://tildes.net";
+ let path = "";
+
+ switch (tourId) {
+ case "interface-homepage":
+ case "introduction": {
+ path = "/";
+ break;
+ }
+
+ default:
+ }
+
+ return `${baseUrl}${path}${anchor}`;
+}
+
+export class Tour extends Component {
+ render() {
+ const {hasBeenCompleted, name, tourId} = this.props;
+ const classes = ["tour", hasBeenCompleted ? "completed" : ""].join(" ");
+ const completed = hasBeenCompleted ? (
+
+ ✔
+
+ ) : undefined;
+
+ return (
+
+
{name}
+ {completed}
+ {tourDescription(tourId)}
+
+ Take this tour
+
+
+ );
+ }
+}
diff --git a/source/options/components/tours.tsx b/source/options/components/tours.tsx
new file mode 100644
index 0000000..35d7e18
--- /dev/null
+++ b/source/options/components/tours.tsx
@@ -0,0 +1,52 @@
+import {Component, type JSX} from "preact";
+import {createToursCompleted} from "../../storage/common.js";
+import {Tour} from "./tour.js";
+
+type Props = Record;
+
+type State = {
+ toursCompleted: Awaited>["value"];
+};
+
+export class Tours extends Component {
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ toursCompleted: new Set(),
+ };
+ }
+
+ async componentDidMount(): Promise {
+ const toursCompleted = await createToursCompleted();
+ this.setState({toursCompleted: toursCompleted.value});
+ }
+
+ render(): JSX.Element {
+ const {toursCompleted} = this.state;
+
+ const createTour = (tourId: TourId, name: string): Tour["props"] => {
+ return {
+ hasBeenCompleted: toursCompleted.has(tourId),
+ name,
+ tourId,
+ };
+ };
+
+ const tourProps: Array = [
+ createTour("introduction", "Introduction"),
+ createTour("interface-homepage", "The Homepage"),
+ ];
+
+ return (
+
+ Tours
+
+ {tourProps.map((props) => (
+
+ ))}
+
+
+ );
+ }
+}
diff --git a/source/options/setup.tsx b/source/options/setup.tsx
new file mode 100644
index 0000000..a132e73
--- /dev/null
+++ b/source/options/setup.tsx
@@ -0,0 +1,8 @@
+import {render} from "preact";
+import "modern-normalize/modern-normalize.css";
+import "../scss/global.scss";
+import {App} from "./app.js";
+
+const preactRoot = document.createElement("div");
+document.body.append(preactRoot);
+render(, preactRoot);
diff --git a/source/scss/components/page-header.scss b/source/scss/components/page-header.scss
new file mode 100644
index 0000000..7e15f84
--- /dev/null
+++ b/source/scss/components/page-header.scss
@@ -0,0 +1,11 @@
+.page-header {
+ display: flex;
+ margin-top: 16px;
+ padding: 0 16px;
+
+ img {
+ aspect-ratio: 1;
+ height: 5rem;
+ margin-right: 1rem;
+ }
+}
diff --git a/source/scss/components/tours.scss b/source/scss/components/tours.scss
new file mode 100644
index 0000000..16d0943
--- /dev/null
+++ b/source/scss/components/tours.scss
@@ -0,0 +1,61 @@
+@use "sass:list";
+
+.tours {
+ display: grid;
+ gap: 16px;
+ grid-template-columns: repeat(3, 1fr);
+ margin-top: 16px;
+}
+
+.tour {
+ background-color: var(--background-secondary);
+ border: 2px solid var(--tour-accent-color);
+ display: grid;
+ gap: 8px;
+ grid-template-columns: auto;
+ grid-template-rows: min-content auto min-content;
+ padding: 16px;
+
+ &.completed {
+ grid-template-columns: auto min-content;
+
+ .tour-description,
+ .tour-link {
+ grid-column: 1 / 3;
+ }
+ }
+
+ $border-colors: (
+ "red",
+ "orange",
+ "yellow",
+ "green",
+ "cyan",
+ "blue",
+ "violet",
+ "magenta",
+ );
+
+ @each $color in $border-colors {
+ $color-number: list.index($border-colors, $color);
+
+ &:nth-child(#{list.length($border-colors)}n + #{$color-number}) {
+ --tour-accent-color: var(--#{$color});
+ --tour-light-accent-color: var(--light-#{$color});
+ }
+ }
+
+ .tour-link a {
+ color: var(--tour-light-accent-color);
+ font-weight: bold;
+ text-decoration: underline;
+
+ &:hover {
+ color: var(--foreground);
+ }
+ }
+
+ .tour-completed {
+ color: var(--light-green);
+ }
+}
diff --git a/source/scss/global.scss b/source/scss/global.scss
new file mode 100644
index 0000000..55b2ffc
--- /dev/null
+++ b/source/scss/global.scss
@@ -0,0 +1,56 @@
+@use "reset";
+@use "components/page-header";
+@use "components/tours";
+
+html {
+ font-size: 62.5%;
+}
+
+body {
+ --background-primary: #00171d;
+ --background-secondary: #002b36;
+ --background-tertiary: #000;
+ --foreground: #fdf6e3;
+ --red: #dc322f;
+ --light-red: #e35d5b;
+ --dark-red: #b9221f;
+ --orange: #cb4b16;
+ --light-orange: #e8632c;
+ --dark-orange: #9d3a11;
+ --yellow: #b58900;
+ --light-yellow: #e8b000;
+ --dark-yellow: #826200;
+ --green: #859900;
+ --light-green: #b1cc00;
+ --dark-green: #596600;
+ --cyan: #2aa198;
+ --light-cyan: #35c9be;
+ --dark-cyan: #1f7972;
+ --blue: #268bd2;
+ --light-blue: #4ca2df;
+ --dark-blue: #1e6ea7;
+ --violet: #6c71c4;
+ --light-violet: #9094d3;
+ --dark-violet: #484fb5;
+ --magenta: #d33682;
+ --light-magenta: #dc609c;
+ --dark-magenta: #b02669;
+
+ background-color: var(--background-primary);
+ color: var(--foreground);
+ font-size: 2rem;
+}
+
+a {
+ color: var(--accent-1);
+ text-decoration: none;
+
+ &:hover {
+ color: var(--accent-2);
+ text-decoration: underline;
+ }
+}
+
+main {
+ padding: 16px;
+}
diff --git a/source/scss/reset.scss b/source/scss/reset.scss
new file mode 100644
index 0000000..83a98f6
--- /dev/null
+++ b/source/scss/reset.scss
@@ -0,0 +1,15 @@
+blockquote,
+code,
+h1,
+h2,
+h3,
+h4,
+h5,
+li,
+ol,
+p,
+pre,
+ul {
+ margin: 0;
+ padding: 0;
+}