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; } } }