Initial skeleton
This commit is contained in:
commit
cb5edfe635
|
@ -0,0 +1,6 @@
|
|||
*~
|
||||
*.orig
|
||||
*.rej
|
||||
|
||||
/node_modules
|
||||
/dist
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "work-it-out",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "CC-BY-SA-4.0",
|
||||
"browser": "index.js",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"css-loader": "^7.1.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"inkjs": "^2.3",
|
||||
"sass": "^1.77.8",
|
||||
"sass-loader": "^16.0.1",
|
||||
"style-loader": "^4.0.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"build": "webpack --mode production",
|
||||
"dev": "webpack serve --mode development",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.0.0",
|
||||
"pnpm": ">= 9.0"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,4 @@
|
|||
declare module '*.ink' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo="><!-- Empty favicon -->
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="outerContainer">
|
||||
<h3 class="written-in-ink"><a href="https://www.inklestudios.com/ink">WRITTEN IN INK</a></h3>
|
||||
|
||||
<div id="controls" class="buttons">
|
||||
<a id="rewind" title="Restart story from beginning">restart</a>
|
||||
<a id="save" title="Save progress">save</a>
|
||||
<a id="reload" title="Reload from save point">load</a>
|
||||
<a id="theme-switch" title="Switch theme">theme</a>
|
||||
</div>
|
||||
|
||||
<div id="story" class="container">
|
||||
<div class="header">
|
||||
<h1>Work It Out</h1>
|
||||
<h2 class="byline"></h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,434 @@
|
|||
// This file was adapted to Typescript from the original
|
||||
// example from https://github.com/inkle/inky/blob/858f7e545f864d114ed2bc662e0948a53461695c/app/export-for-web-template/main.js
|
||||
// Copyright (c) 2016 inkle Ltd.
|
||||
// Under the MIT license.
|
||||
|
||||
import { Compiler } from 'inkjs/compiler/Compiler';
|
||||
|
||||
import "./style.scss";
|
||||
import data from '../story/main.ink';
|
||||
|
||||
const story = new Compiler(data).Compile();
|
||||
|
||||
// Global tags - those at the top of the ink file
|
||||
// We support:
|
||||
// # theme: dark
|
||||
// # author: Your Name
|
||||
var globalTagTheme: string | null = null;
|
||||
story.globalTags?.forEach((globalTag) => {
|
||||
let splitTag = splitPropertyTag(globalTag);
|
||||
switch (splitTag?.property) {
|
||||
case "theme":
|
||||
globalTagTheme = splitTag.val;
|
||||
break;
|
||||
case "author":
|
||||
var byline = document.querySelector('.byline')!;
|
||||
byline.innerHTML = "by " + splitTag.val;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
setupTheme(globalTagTheme);
|
||||
|
||||
var storyContainer = document.getElementById('story')!;
|
||||
var outerScrollContainer = document.querySelector<HTMLElement>('.outerContainer')!;
|
||||
|
||||
// page features setup
|
||||
var hasSave = loadSavePoint();
|
||||
setupButtons(hasSave);
|
||||
|
||||
// Set initial save point
|
||||
var savePoint = story.state.toJson();
|
||||
|
||||
// Kick off the start of the story!
|
||||
continueStory(true);
|
||||
|
||||
// Main story processing function. Each time this is called it generates
|
||||
// all the next content up as far as the next set of choices.
|
||||
function continueStory(this: any, firstTime: boolean) {
|
||||
var paragraphIndex = 0;
|
||||
var delay = 0.0;
|
||||
|
||||
// Don't over-scroll past new content
|
||||
var previousBottomEdge = firstTime ? 0 : contentBottomEdgeY();
|
||||
|
||||
// Generate story text - loop through available content
|
||||
while (story.canContinue) {
|
||||
|
||||
// Get ink to generate the next paragraph
|
||||
var paragraphText = story.Continue()!;
|
||||
|
||||
// Any special tags included with this line
|
||||
var customClasses: string[] = [];
|
||||
|
||||
story.currentTags?.forEach((tag) => {
|
||||
// Detect tags of the form "X: Y". Currently used for IMAGE and CLASS but could be
|
||||
// customised to be used for other things too.
|
||||
var splitTag = splitPropertyTag(tag);
|
||||
|
||||
switch (splitTag?.property.toUpperCase()) {
|
||||
// AUDIO: src
|
||||
case "AUDIO":
|
||||
if ('audio' in this) {
|
||||
this.audio.pause();
|
||||
this.audio.removeAttribute('src');
|
||||
this.audio.load();
|
||||
}
|
||||
|
||||
this.audio = new Audio(splitTag.val);
|
||||
this.audio.play();
|
||||
break;
|
||||
|
||||
|
||||
// AUDIOLOOP: src
|
||||
case "AUDIOLOOP":
|
||||
if ('audioLoop' in this) {
|
||||
this.audioLoop.pause();
|
||||
this.audioLoop.removeAttribute('src');
|
||||
this.audioLoop.load();
|
||||
}
|
||||
this.audioLoop = new Audio(splitTag.val);
|
||||
this.audioLoop.play();
|
||||
this.audioLoop.loop = true;
|
||||
break;
|
||||
|
||||
// IMAGE: src
|
||||
case "IMAGE":
|
||||
var imageElement = document.createElement('img');
|
||||
imageElement.src = splitTag.val;
|
||||
storyContainer.appendChild(imageElement);
|
||||
|
||||
imageElement.onload = () => {
|
||||
console.log(`scrollingto ${previousBottomEdge}`)
|
||||
scrollDown(previousBottomEdge)
|
||||
}
|
||||
|
||||
showAfter(delay, imageElement);
|
||||
delay += 200.0;
|
||||
break;
|
||||
|
||||
// LINK: url
|
||||
case "LINK":
|
||||
window.location.href = splitTag.val;
|
||||
break;
|
||||
|
||||
|
||||
// LINKOPEN: url
|
||||
case "LINKOPEN":
|
||||
window.open(splitTag.val);
|
||||
break;
|
||||
|
||||
// BACKGROUND: src
|
||||
case "BACKGROUND":
|
||||
outerScrollContainer.style.backgroundImage = 'url(' + splitTag.val + ')';
|
||||
break;
|
||||
|
||||
// CLASS: className
|
||||
case "CLASS":
|
||||
customClasses.push(splitTag.val);
|
||||
break;
|
||||
|
||||
// CLEAR - removes all existing content.
|
||||
case "CLEAR":
|
||||
clearStory();
|
||||
break;
|
||||
|
||||
// RESTART - clears everything and restarts the story from the beginning
|
||||
case "RESTART":
|
||||
restart();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if paragraphText is empty
|
||||
if (paragraphText?.trim().length == 0) {
|
||||
continue; // Skip empty paragraphs
|
||||
}
|
||||
|
||||
// Create paragraph element (initially hidden)
|
||||
var paragraphElement = document.createElement('p');
|
||||
paragraphElement.innerHTML = paragraphText;
|
||||
storyContainer.appendChild(paragraphElement);
|
||||
|
||||
// Add any custom classes derived from ink tags
|
||||
for (var i = 0; i < customClasses.length; i++)
|
||||
paragraphElement.classList.add(customClasses[i]);
|
||||
|
||||
// Fade in paragraph after a short delay
|
||||
showAfter(delay, paragraphElement);
|
||||
delay += 200.0;
|
||||
}
|
||||
|
||||
// Create HTML choices from ink choices
|
||||
story.currentChoices.forEach((choice) => {
|
||||
|
||||
// Create paragraph with anchor element
|
||||
var customClasses: string[] = [];
|
||||
var isClickable = true;
|
||||
choice.tags?.forEach((choiceTag) => {
|
||||
var splitTag = splitPropertyTag(choiceTag);
|
||||
switch(splitTag?.property.toUpperCase()) {
|
||||
|
||||
case "UNCLICKABLE":
|
||||
isClickable = false;
|
||||
break;
|
||||
|
||||
|
||||
case "CLASS":
|
||||
customClasses.push(splitTag.val);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
var choiceParagraphElement = document.createElement('p');
|
||||
choiceParagraphElement.classList.add("choice");
|
||||
|
||||
customClasses.forEach((klass) => {
|
||||
choiceParagraphElement.classList.add(klass);
|
||||
});
|
||||
|
||||
if (isClickable) {
|
||||
choiceParagraphElement.innerHTML = `<a href='#'>${choice.text}</a>`
|
||||
} else {
|
||||
choiceParagraphElement.innerHTML = `<span class='unclickable'>${choice.text}</span>`
|
||||
}
|
||||
storyContainer.appendChild(choiceParagraphElement);
|
||||
|
||||
// Fade choice in after a short delay
|
||||
showAfter(delay, choiceParagraphElement);
|
||||
delay += 200.0;
|
||||
|
||||
// Click on choice
|
||||
if (isClickable) {
|
||||
var choiceAnchorEl = choiceParagraphElement.querySelectorAll("a")[0];
|
||||
choiceAnchorEl.addEventListener("click", function (event) {
|
||||
|
||||
// Don't follow <a> link
|
||||
event.preventDefault();
|
||||
|
||||
// Extend height to fit
|
||||
// We do this manually so that removing elements and creating new ones doesn't
|
||||
// cause the height (and therefore scroll) to jump backwards temporarily.
|
||||
storyContainer.style.height = contentBottomEdgeY() + "px";
|
||||
|
||||
// Remove all existing choices
|
||||
removeAll(".choice");
|
||||
|
||||
// Tell the story where to go next
|
||||
story.ChooseChoiceIndex(choice.index);
|
||||
|
||||
// This is where the save button will save from
|
||||
savePoint = story.state.toJson();
|
||||
|
||||
// Aaand loop
|
||||
continueStory(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Unset storyContainer's height, allowing it to resize itself
|
||||
storyContainer.style.height = "";
|
||||
|
||||
if (!firstTime)
|
||||
scrollDown(previousBottomEdge);
|
||||
}
|
||||
|
||||
function clearStory() {
|
||||
removeAll("p");
|
||||
removeAll("img");
|
||||
|
||||
// Comment out this line if you want to leave the header visible when clearing
|
||||
setVisible(".header", false);
|
||||
}
|
||||
|
||||
function restart() {
|
||||
clearStory();
|
||||
|
||||
story.ResetState();
|
||||
|
||||
setVisible(".header", true);
|
||||
|
||||
// set save point to here
|
||||
savePoint = story.state.toJson();
|
||||
|
||||
continueStory(true);
|
||||
|
||||
outerScrollContainer.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// Various Helper functions
|
||||
// -----------------------------------
|
||||
|
||||
// Detects whether the user accepts animations
|
||||
function isAnimationEnabled() {
|
||||
return window.matchMedia('(prefers-reduced-motion: no-preference)').matches;
|
||||
}
|
||||
|
||||
// Fades in an element after a specified delay
|
||||
function showAfter(delay: number, el: Element) {
|
||||
if (isAnimationEnabled()) {
|
||||
el.classList.add("hide");
|
||||
setTimeout(function () { el.classList.remove("hide") }, delay);
|
||||
} else {
|
||||
// If the user doesn't want animations, show immediately
|
||||
el.classList.remove("hide");
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolls the page down, but no further than the bottom edge of what you could
|
||||
// see previously, so it doesn't go too far.
|
||||
function scrollDown(previousBottomEdge: number) {
|
||||
// If the user doesn't want animations, let them scroll manually
|
||||
if (!isAnimationEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Line up top of screen with the bottom of where the previous content ended
|
||||
var target = previousBottomEdge;
|
||||
|
||||
// Can't go further than the very bottom of the page
|
||||
var limit = outerScrollContainer.scrollHeight - outerScrollContainer.clientHeight;
|
||||
if (target > limit) target = limit;
|
||||
|
||||
var start = outerScrollContainer.scrollTop;
|
||||
|
||||
var dist = target - start;
|
||||
var duration = 300 + 300 * dist / 100;
|
||||
var startTime: EpochTimeStamp | null = null;
|
||||
function step(time: EpochTimeStamp) {
|
||||
if (startTime == null) startTime = time;
|
||||
var t = (time - startTime) / duration;
|
||||
var lerp = 3 * t * t - 2 * t * t * t; // ease in/out
|
||||
outerScrollContainer.scrollTo(0, (1.0 - lerp) * start + lerp * target);
|
||||
if (t < 1) requestAnimationFrame(step);
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
// The Y coordinate of the bottom end of all the story content, used
|
||||
// for growing the container, and deciding how far to scroll.
|
||||
function contentBottomEdgeY() {
|
||||
var bottomElement = storyContainer.lastElementChild as HTMLElement;
|
||||
return bottomElement ? bottomElement.offsetTop + bottomElement.offsetHeight : 0;
|
||||
}
|
||||
|
||||
// Remove all elements that match the given selector. Used for removing choices after
|
||||
// you've picked one, as well as for the CLEAR and RESTART tags.
|
||||
function removeAll(selector: string) {
|
||||
storyContainer.querySelectorAll<HTMLElement>(selector).forEach((el) => {
|
||||
el.parentNode!.removeChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
// Used for hiding and showing the header when you CLEAR or RESTART the story respectively.
|
||||
function setVisible(selector: string, visible: boolean) {
|
||||
var allElements = storyContainer.querySelectorAll(selector);
|
||||
for (var i = 0; i < allElements.length; i++) {
|
||||
var el = allElements[i];
|
||||
if (!visible) {
|
||||
el.classList.add("invisible");
|
||||
} else {
|
||||
el.classList.remove("invisible");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for parsing out tags of the form:
|
||||
// # PROPERTY: value
|
||||
// e.g. IMAGE: source path
|
||||
function splitPropertyTag(tag: string) {
|
||||
var propertySplitIdx = tag.indexOf(":");
|
||||
if (propertySplitIdx != null) {
|
||||
var property = tag.slice(0, propertySplitIdx).trim();
|
||||
var val = tag.slice(propertySplitIdx + 1).trim();
|
||||
return {
|
||||
property: property,
|
||||
val: val
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Loads save state if exists in the browser memory
|
||||
function loadSavePoint() {
|
||||
|
||||
try {
|
||||
let savedState = window.localStorage.getItem('save-state');
|
||||
if (savedState) {
|
||||
story.state.LoadJson(savedState);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug("Couldn't load save state");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detects which theme (light or dark) to use
|
||||
function setupTheme(globalTagTheme: string | null) {
|
||||
|
||||
// load theme from browser memory
|
||||
var savedTheme;
|
||||
try {
|
||||
savedTheme = window.localStorage.getItem('theme');
|
||||
} catch (e) {
|
||||
console.debug("Couldn't load saved theme");
|
||||
}
|
||||
|
||||
// Check whether the OS/browser is configured for dark mode
|
||||
var browserDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
let preferredTheme = savedTheme ?? globalTagTheme ?? (browserDark ? "dark" : "light");
|
||||
document.body.classList.add(preferredTheme);
|
||||
}
|
||||
|
||||
// Used to hook up the functionality for global functionality buttons
|
||||
function setupButtons(hasSave: boolean) {
|
||||
|
||||
let rewindEl = document.getElementById("rewind");
|
||||
if (rewindEl) rewindEl.addEventListener("click", function (event) {
|
||||
removeAll("p");
|
||||
removeAll("img");
|
||||
setVisible(".header", false);
|
||||
restart();
|
||||
});
|
||||
|
||||
let saveEl = document.getElementById("save");
|
||||
if (saveEl) saveEl.addEventListener("click", function (event) {
|
||||
try {
|
||||
window.localStorage.setItem('save-state', savePoint);
|
||||
document.getElementById("reload")!.removeAttribute("disabled");
|
||||
window.localStorage.setItem('theme', document.body.classList.contains("dark") ? "dark" : "");
|
||||
} catch (e) {
|
||||
console.warn("Couldn't save state");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
let reloadEl = document.getElementById("reload")!;
|
||||
if (!hasSave) {
|
||||
reloadEl.setAttribute("disabled", "disabled");
|
||||
}
|
||||
reloadEl.addEventListener("click", function (event) {
|
||||
if (reloadEl.getAttribute("disabled"))
|
||||
return;
|
||||
|
||||
removeAll("p");
|
||||
removeAll("img");
|
||||
try {
|
||||
let savedState = window.localStorage.getItem('save-state');
|
||||
if (savedState) story.state.LoadJson(savedState);
|
||||
} catch (e) {
|
||||
console.debug("Couldn't load save state");
|
||||
}
|
||||
continueStory(true);
|
||||
});
|
||||
|
||||
let themeSwitchEl = document.getElementById("theme-switch");
|
||||
if (themeSwitchEl) themeSwitchEl.addEventListener("click", function (event) {
|
||||
document.body.classList.add("switched");
|
||||
document.body.classList.toggle("dark");
|
||||
});
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,700|Quattrocento:700');
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: lighter;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
body.switched {
|
||||
transition: color 0.6s, background-color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
text-align: center;
|
||||
font-family: "Quattrocento", Georgia, 'Times New Roman', Times, serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30pt;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14pt;
|
||||
font-style: italic;
|
||||
font-family: sans-serif;
|
||||
font-weight: lighter;
|
||||
color: #BBB;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-top: 3em;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
|
||||
/*
|
||||
Built-in class:
|
||||
# author: Name
|
||||
*/
|
||||
.byline {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.written-in-ink {
|
||||
z-index: 3;
|
||||
font-size: 9pt;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
position: fixed;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
height: 14px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.written-in-ink {
|
||||
transition: color 0.6s, background 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Enables <iframe> support work on itch.io when using mobile iOS
|
||||
*/
|
||||
.outerContainer {
|
||||
position: absolute;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin-top: 24px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
.outerContainer {
|
||||
margin-top: 44px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
padding-top: 4em;
|
||||
background: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.switched .container {
|
||||
transition: background-color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 13pt;
|
||||
color: #888;
|
||||
line-height: 1.7em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 700;
|
||||
color: #b97c2c;
|
||||
font-family: sans-serif;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.unclickable {
|
||||
font-weight: 700;
|
||||
color: #4f3411;
|
||||
font-family: sans-serif;
|
||||
text-decoration: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
a {
|
||||
transition: color 0.6s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
transition: color 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.container .hide {
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
.container .invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container>* {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.container>* {
|
||||
transition: opacity 1.0s;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to all choices
|
||||
(Will always appear inside <p> element by default.)
|
||||
*/
|
||||
.choice {
|
||||
text-align: center;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to first choice
|
||||
*/
|
||||
:not(.choice)+.choice {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
/*
|
||||
Class applied to choice links
|
||||
*/
|
||||
.choice a, .choice span {
|
||||
font-size: 15pt;
|
||||
}
|
||||
|
||||
/*
|
||||
Built-in class:
|
||||
The End # CLASS: end
|
||||
*/
|
||||
.end {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
z-index: 4;
|
||||
font-size: 9pt;
|
||||
text-align: center;
|
||||
padding-bottom: 6px;
|
||||
position: fixed;
|
||||
right: 14px;
|
||||
top: 4px;
|
||||
user-select: none;
|
||||
background: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
#controls {
|
||||
transition: color 0.6s, background 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
#controls [disabled] {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
#controls>*:not(:last-child):after {
|
||||
content: " | ";
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
#controls {
|
||||
z-index: 2;
|
||||
padding-top: 24px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Dark Theme (Added in Inky 0.10.0)
|
||||
# theme: dark
|
||||
*/
|
||||
|
||||
body.dark {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark h2 {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dark .container {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.dark .written-in-ink {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.dark a {
|
||||
color: #cc8f1a;
|
||||
}
|
||||
.dark .unclickable{
|
||||
color: #c4af87;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
.dark a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
.dark a {
|
||||
transition: color 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
.dark strong {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark #controls [disabled] {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.dark .end {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark #controls {
|
||||
background: black;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
# author: Matteo Settenvini
|
||||
|
||||
-> intro
|
||||
|
||||
=== intro ===
|
||||
|
||||
We were thinking that. And again, and again.
|
||||
|
||||
+ Maybe yes -> ending
|
||||
+ Maybe not -> intro
|
||||
|
||||
-> ending
|
||||
|
||||
=== ending ===
|
||||
|
||||
Goodbye then.
|
||||
|
||||
-> END
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["es2023", "DOM"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.ts', // Entry file
|
||||
output: {
|
||||
filename: 'bundle.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.ink$/,
|
||||
type: 'asset/source',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.s[ac]ss$/,
|
||||
use: [
|
||||
"style-loader", // Creates `style` nodes from JS strings
|
||||
"css-loader", // Translates CSS into CommonJS
|
||||
"sass-loader", // Compiles Sass to CSS
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
title: "Work It Out",
|
||||
template: "src/index.html",
|
||||
})
|
||||
],
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, 'dist'),
|
||||
},
|
||||
liveReload: true,
|
||||
compress: true,
|
||||
port: 9000,
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue