diff --git a/assets/images/tab-icons/chemistry.svg b/assets/images/tab-icons/chemistry.svg
new file mode 100644
index 0000000..4faaaa8
--- /dev/null
+++ b/assets/images/tab-icons/chemistry.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/assets/images/tab-icons/unknown.svg b/assets/images/tab-icons/unknown.svg
new file mode 100644
index 0000000..373a01b
--- /dev/null
+++ b/assets/images/tab-icons/unknown.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/assets/projectfiles/icons.afdesign b/assets/projectfiles/icons.afdesign
new file mode 100644
index 0000000..f57e593
Binary files /dev/null and b/assets/projectfiles/icons.afdesign differ
diff --git a/assets/projectfiles/logo.afdesign b/assets/projectfiles/logo.afdesign
new file mode 100644
index 0000000..75bfcc6
Binary files /dev/null and b/assets/projectfiles/logo.afdesign differ
diff --git a/index.html b/index.html
index 8805a3c..9ede1f2 100644
--- a/index.html
+++ b/index.html
@@ -43,12 +43,7 @@
-
+
/tg/station Handbook
@@ -58,6 +53,7 @@
+
diff --git a/src/TabManager.ts b/src/TabManager.ts
index 0b038e0..9053f1c 100644
--- a/src/TabManager.ts
+++ b/src/TabManager.ts
@@ -3,6 +3,9 @@ import speen from "~/assets/images/speen.svg";
import { getPageHTML } from "./wiki";
import userscript from "./userscript";
+// @ts-expect-error: Parcel image import
+import unknown from "~/assets/images/tab-icons/unknown.svg";
+
function initWaiting(elem: HTMLElement) {
// Add spinner
const spinnerContainer = document.createElement("div");
@@ -36,32 +39,117 @@ async function loadPage(page: string, elem: HTMLElement) {
});
}
-type TabElements = { tabListItem: HTMLElement; tabContentItem: HTMLElement };
+type TabElements = {
+ tabListItem: HTMLElement;
+ tabContentItem: HTMLElement;
+};
+
+interface Section {
+ name: string;
+ element: HTMLElement;
+ tabs: Record;
+}
export default class TabManager {
+ sectionListContainer: HTMLElement;
+
tabListContainer: HTMLElement;
tabContentContainer: HTMLElement;
- tabs: Record = {};
+ sections: Record = {};
- constructor(tablist: HTMLElement, tabcontent: HTMLElement) {
+ constructor(
+ sectionlist: HTMLElement,
+ tablist: HTMLElement,
+ tabcontent: HTMLElement
+ ) {
+ this.sectionListContainer = sectionlist;
this.tabListContainer = tablist;
this.tabContentContainer = tabcontent;
}
- openTab(page: string, setActive: boolean): void {
+ /**
+ * Create section and add it to the section list
+ * @param name Section name
+ */
+ createSection(name: string): void {
+ // Create section element
+ const sectionItem = document.createElement("div");
+ sectionItem.className = "section";
+ sectionItem.dataset.section = name;
+ sectionItem.appendChild(document.createTextNode(name));
+ sectionItem.addEventListener("click", () => {
+ if (sectionItem.classList.contains("active")) {
+ return;
+ }
+ this.showSection(name);
+ });
+ this.sectionListContainer.appendChild(sectionItem);
+
+ this.sections[name] = { name, element: sectionItem, tabs: {} };
+ }
+
+ /**
+ * Show tabs of a specific section
+ * @param name Section name
+ */
+ showSection(name: string): void {
+ const active = this.sectionListContainer.querySelector(
+ ".active"
+ );
+ if (active) {
+ // De-activate current section
+ active.classList.remove("active");
+
+ // Hide all tabs
+ this.tabListContainer
+ .querySelectorAll(`div[data-section=${active.dataset.section}]`)
+ .forEach((tab) => tab.classList.add("hidden"));
+ }
+ // Set section as active
+ this.sections[name].element.classList.add("active");
+
+ // Show all tabs of that section
+ this.tabListContainer
+ .querySelectorAll(`div[data-section=${name}]`)
+ .forEach((tab) => tab.classList.remove("hidden"));
+ }
+
+ /**
+ * Open tab page and add it to the tab list
+ * @param section Section to add the tab button to
+ * @param page Page name
+ * @param icon Icon to show
+ * @param setActive Also set the tab as active
+ */
+ openTab(
+ section: string,
+ page: string,
+ options: {
+ icon?: string;
+ active?: boolean;
+ text?: string;
+ }
+ ): void {
// Create tab list item
const tabListItem = document.createElement("div");
tabListItem.className = "tab";
+ tabListItem.dataset.section = section;
tabListItem.dataset.tab = page;
tabListItem.addEventListener("click", () => {
if (tabListItem.classList.contains("active")) {
return;
}
- this.setActive(page);
+ this.setActive(section, page);
});
- tabListItem.appendChild(document.createTextNode(page.replace(/_/gi, " ")));
+ const iconElement = document.createElement("img");
+ iconElement.src = options.icon || unknown;
+ tabListItem.title = page.replace(/_/gi, " ");
+ tabListItem.appendChild(iconElement);
+ const shortTitle =
+ options.text || page.substr(page.lastIndexOf("_") + 1, 4);
+ tabListItem.appendChild(document.createTextNode(shortTitle));
this.tabListContainer.appendChild(tabListItem);
// Create tab content container
@@ -69,27 +157,45 @@ export default class TabManager {
tabContentItem.className = "page waiting";
tabContentItem.dataset.tab = page;
initWaiting(tabContentItem);
- this.tabContentContainer.appendChild(tabContentItem);
// Start loading page for new tab
loadPage(page, tabContentItem);
+ this.tabContentContainer.appendChild(tabContentItem);
+
// Create tab entry
- this.tabs[page] = { tabListItem, tabContentItem };
+ this.sections[section].tabs[page] = { tabListItem, tabContentItem };
+
+ // Hide tab if section is hidden
+ if (!this.sections[section].element.classList.contains("active")) {
+ tabListItem.classList.add("hidden");
+ }
// If asked for, set it to active
- if (setActive) {
- this.setActive(page);
+ if (options.active) {
+ this.setActive(section, page);
}
}
- setActive(page: string): void {
+ /**
+ * Set a specific page to be the active/visible one
+ * @param section Section name
+ * @param page Page name
+ */
+ setActive(section: string, page: string): void {
// Make sure tab exists (why wouldn't it?!)
- if (!(page in this.tabs)) {
+ if (!(section in this.sections)) {
+ throw new Error("section not found");
+ }
+
+ if (!(page in this.sections[section].tabs)) {
throw new Error("tab not found");
}
// Deactivate current active tab
+ this.sectionListContainer
+ .querySelectorAll(".active")
+ .forEach((it) => it.classList.remove("active"));
this.tabListContainer
.querySelectorAll(".active")
.forEach((it) => it.classList.remove("active"));
@@ -97,8 +203,14 @@ export default class TabManager {
.querySelectorAll(".active")
.forEach((it) => it.classList.remove("active"));
+ // If section is not shown, show it!
+ if (!this.sections[section].element.classList.contains("active")) {
+ this.showSection(section);
+ }
+
// Activate new tab
- const { tabListItem, tabContentItem } = this.tabs[page];
+ const { tabListItem, tabContentItem } = this.sections[section].tabs[page];
+ this.sections[section].element.classList.add("active");
tabListItem.classList.add("active");
tabContentItem.classList.add("active");
}
diff --git a/src/index.ts b/src/index.ts
index f2500ba..890322e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,18 +1,25 @@
import TabManager from "./TabManager";
+import sections from "./sections";
+const sectionListContainer = document.getElementById("section-list");
const tabListContainer = document.getElementById("tab-list");
const tabContentContainer = document.getElementById("tabs");
-const manager = new TabManager(tabListContainer, tabContentContainer);
+const manager = new TabManager(
+ sectionListContainer,
+ tabListContainer,
+ tabContentContainer
+);
-const defaultTabs = [
- { page: "Guide_to_chemistry", active: true },
- { page: "Guide_to_medicine", active: false },
-];
-
-defaultTabs.forEach((tab) => {
- manager.openTab(tab.page, tab.active);
+sections.forEach((section) => {
+ manager.createSection(section.name);
+ section.tabs.forEach((tab) => {
+ manager.openTab(section.name, tab.page, { icon: tab.icon, text: tab.text });
+ });
});
+// Set first page as active
+manager.setActive("Medical", "Guide_to_chemistry");
+
if ("serviceWorker" in navigator) {
const x = process.env.SUBPATH ? `${process.env.SUBPATH}/sw.js` : "sw.js";
navigator.serviceWorker
diff --git a/src/sections.ts b/src/sections.ts
new file mode 100644
index 0000000..a6e50ac
--- /dev/null
+++ b/src/sections.ts
@@ -0,0 +1,104 @@
+// @ts-expect-error: Parcel image import
+import chemistry from "~/assets/images/tab-icons/chemistry.svg";
+
+const sections = [
+ {
+ name: "Medical",
+ tabs: [
+ { page: "Guide_to_medicine", icon: null },
+ { page: "Guide_to_chemistry", icon: chemistry },
+ { page: "Guide_to_plumbing", icon: null },
+ { page: "Grenade", icon: null },
+ { page: "Guide_to_genetics", icon: null },
+ { page: "Infections", text: "VIRS", icon: null },
+ { page: "Surgery", icon: null },
+ { page: "Guide_to_Traumas", icon: null },
+ { page: "Guide_to_Wounds", icon: null },
+ { page: "Guide_to_Ghetto_Chemistry", text: "ghet", icon: null },
+ ],
+ },
+ {
+ name: "Engineering",
+ tabs: [
+ { page: "Guide_to_construction", icon: null },
+ {
+ page: "Guide_to_advanced_construction",
+ text: "mach",
+ icon: null,
+ },
+ { page: "Solars", icon: null },
+ { page: "Guide_to_the_Supermatter", text: "SUPM", icon: null },
+ { page: "Singularity_Engine", text: "SING", icon: null },
+ { page: "Tesla_Engine", text: "TESL", icon: null },
+ { page: "Gas_turbine", text: "GAS", icon: null },
+ { page: "Guide_to_power", icon: null },
+ { page: "Guide_to_Atmospherics", icon: null },
+ { page: "Guide_to_Telecommunications", icon: null, text: "tcomm" },
+ ],
+ },
+ {
+ name: "Science",
+ tabs: [
+ { page: "Guide_to_Research_and_Development", text: "R&D", icon: null },
+ { page: "Guide_to_robotics", icon: null },
+ { page: "Guide_to_toxins", icon: null },
+ { page: "Guide_to_xenobiology", icon: null },
+ { page: "Guide_to_genetics", icon: null },
+ { page: "Guide_to_telescience", icon: null },
+ { page: "Guide_to_Nanites", icon: null },
+ ],
+ },
+ {
+ name: "Security",
+ tabs: [
+ { page: "Guide_to_security", icon: null },
+ { page: "Space_Law", text: "LAW", icon: null },
+ { page: "Standard_Operating_Procedure", text: "SOP", icon: null },
+ { page: "Guide_to_Trials", icon: null },
+ ],
+ },
+ {
+ name: "Antagonists",
+ tabs: [
+ { page: "Traitor", icon: null },
+ { page: "Makeshift_weapons", icon: null },
+ { page: "Hacking", icon: null },
+ { page: "Guide_to_Combat", icon: null },
+ { page: "Syndicate_Items", text: "synd", icon: null },
+ { page: "Illicit_Access", icon: null },
+ { page: "Revolution", icon: null },
+ { page: "Cult_Basics", text: "cult", icon: null },
+ { page: "Syndicate_guide", text: "nuke", icon: null },
+ { page: "Guide_to_malfunction", icon: null },
+ { page: "Xenos", text: "xmor", icon: null },
+ { page: "Abductor", icon: null },
+ { page: "Families", icon: null },
+ { page: "Heretic", icon: null },
+ ],
+ },
+ {
+ name: "Other",
+ tabs: [
+ { page: "Ai_Modules", text: "aimo", icon: null },
+ { page: "Silicon_Policy", text: "sipo", icon: null },
+ {
+ page: "Guide_to_Awesome_Miscellaneous_Stuff",
+ text: "misc",
+ icon: null,
+ },
+ { page: "Creatures", icon: null },
+ { page: "Critters", icon: null },
+ { page: "Guide_to_races", icon: null },
+ { page: "Guide_to_food_and_drinks", text: "food", icon: null },
+ { page: "Guide_to_hydroponics", icon: null },
+ { page: "Guide_to_plants", icon: null },
+ { page: "Songs", icon: null },
+ { page: "Supply_crates", icon: null },
+ { page: "Auxiliary_Base_Construction", text: "aux", icon: null },
+ { page: "Guide_to_wire_art", text: "wire", icon: null },
+ { page: "Guide_to_Space_Exploration", icon: null },
+ ],
+ },
+];
+
+export default sections;
diff --git a/src/userscript.ts b/src/userscript.ts
index 43a1de8..6c24238 100644
--- a/src/userscript.ts
+++ b/src/userscript.ts
@@ -3,6 +3,13 @@ import searchBox from "./search";
import { findParent } from "./utils";
export default function userscript(root: HTMLElement, docname: string): void {
+ // Add header
+ const header = document.createElement("h1");
+ header.className = "pageheader";
+ header.appendChild(document.createTextNode(docname.replace(/_/g, " ")));
+ root.insertBefore(header, root.firstChild);
+
+ // Remove edit links
root.querySelectorAll(".mw-editsection").forEach((editLink) => {
editLink.parentElement.removeChild(editLink);
});
@@ -55,8 +62,16 @@ export default function userscript(root: HTMLElement, docname: string): void {
row.appendChild(td);
});
+ // Fuck #toctitle
+ const toc = root.querySelector("#toc");
+ if (toc) {
+ const tocHeader = toc.querySelector("h2");
+ toc.parentNode.insertBefore(tocHeader, toc);
+ toc.removeChild(toc.querySelector("#toctitle"));
+ }
+
// Group headers and content so stickies don't overlap
- root.querySelectorAll("h3,h2").forEach((h3) => {
+ root.querySelectorAll("h1,h2,h3").forEach((h3) => {
const parent = h3.parentNode;
const div = document.createElement("div");
parent.insertBefore(div, h3);
@@ -79,7 +94,7 @@ export default function userscript(root: HTMLElement, docname: string): void {
if (container) {
container.id = span.id;
span.id += "-span";
- container.dataset.name = span.innerText;
+ container.dataset.name = span.textContent;
}
});
@@ -117,7 +132,7 @@ export default function userscript(root: HTMLElement, docname: string): void {
// Enrich "x part" with checkboxes and parts
Array.from(document.querySelectorAll("td"))
- .filter((el) => el.innerText.indexOf(" part") >= 0)
+ .filter((el) => el.textContent.indexOf(" part") >= 0)
.forEach((el) => {
el.innerHTML = el.innerHTML.replace(
/((\d+)\s+(?:parts?|units?))(.*?(?:<\/a>|\n|$))/gi,
diff --git a/style/bgus.scss b/style/bgus.scss
index a18d639..fd58af7 100644
--- a/style/bgus.scss
+++ b/style/bgus.scss
@@ -8,7 +8,7 @@
}
#bgus_fz_searchbox {
position: fixed;
- top: 50px;
+ top: 80px;
left: 20%;
right: 20%;
background: rgba(10, 10, 10, 0.8);
diff --git a/style/main.scss b/style/main.scss
index 59226f8..9eb1319 100644
--- a/style/main.scss
+++ b/style/main.scss
@@ -36,7 +36,6 @@ body {
#app {
height: 100%;
display: grid;
- grid-template-rows: 40px 1fr;
}
::-webkit-scrollbar {
@@ -60,13 +59,12 @@ body {
}
#tabs {
- grid-row: 2;
+ grid-row: 3;
z-index: 1;
display: grid;
overflow: hidden;
.page {
visibility: hidden;
- padding-top: 10pt;
overflow-y: scroll;
grid-row: 1;
grid-column: 1;
@@ -97,6 +95,11 @@ body {
}
}
+ h1.pageheader {
+ margin-top: 0;
+ padding: 15pt 10pt;
+ }
+
p,
h2,
h3,
@@ -107,19 +110,15 @@ body {
a[href] {
color: white;
}
- #toctitle,
h1,
h2,
h3 {
position: sticky;
- top: -10pt;
+ top: 0;
background: $nanotrasen;
- padding: 10px;
+ padding: 10pt;
z-index: 999;
}
- #toctitle h2 {
- margin: 0;
- }
.mw-headline {
display: flex;
align-items: center;
@@ -127,27 +126,73 @@ body {
}
}
+$section-active: darken($nanotrasen, 5%);
$tab-active: lighten($nanotrasen, 10%);
-#tab-list {
+#section-list {
z-index: 2;
grid-row: 1;
+ border-bottom: 2px solid $section-active;
display: flex;
- border-bottom: 2px solid $tab-active;
-
- .tab {
- max-width: 200px;
- flex: 1;
+ .section {
display: flex;
+ flex-direction: column;
align-items: center;
justify-content: center;
user-select: none;
+ font-size: 9pt;
+ padding: 3pt 7pt;
+ text-transform: uppercase;
+ color: lighten($nanotrasen, 60%);
+ flex: 1;
+ &.active {
+ background-color: $section-active;
+ color: white;
+ }
+ &:not(.active) {
+ cursor: pointer;
+ &:hover {
+ background-color: darken($section-active, 10%);
+ }
+ }
+ }
+}
+#tab-list {
+ z-index: 2;
+ grid-row: 2;
+ display: flex;
+ background-color: $section-active;
+ border-bottom: 4px solid $tab-active;
+
+ .tab {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ user-select: none;
+ font-size: 7pt;
+ padding: 2px 4px;
+ padding-bottom: 0;
+ text-transform: uppercase;
+
+ img {
+ height: 80%;
+ max-height: 24px;
+ margin: 0;
+ }
&.active {
background-color: $tab-active;
}
&:not(.active) {
cursor: pointer;
+ &:hover {
+ background-color: darken($tab-active, 8%);
+ }
+ }
+
+ &.hidden {
+ display: none;
}
}
}