├── .eslintrc
├── .gitignore
├── LICENSE.md
├── README.md
├── atrament.config.json
├── index.html
├── jsconfig.json
├── package.json
├── resources
├── fonts
│ ├── fira-sans
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── va9B4kDNxMZdWfMOD5VnLK3eQhf6TF0.woff2
│ │ ├── va9B4kDNxMZdWfMOD5VnLK3eRRf6TF0.woff2
│ │ ├── va9B4kDNxMZdWfMOD5VnLK3eRhf6.woff2
│ │ ├── va9B4kDNxMZdWfMOD5VnLK3eSBf6TF0.woff2
│ │ ├── va9B4kDNxMZdWfMOD5VnLK3eSRf6TF0.woff2
│ │ ├── va9B4kDNxMZdWfMOD5VnLK3eShf6TF0.woff2
│ │ ├── va9B4kDNxMZdWfMOD5VnLK3eSxf6TF0.woff2
│ │ ├── va9C4kDNxMZdWfMOD5VvkrjEYTLHdQ.woff2
│ │ ├── va9C4kDNxMZdWfMOD5VvkrjFYTLHdQ.woff2
│ │ ├── va9C4kDNxMZdWfMOD5VvkrjGYTLHdQ.woff2
│ │ ├── va9C4kDNxMZdWfMOD5VvkrjHYTLHdQ.woff2
│ │ ├── va9C4kDNxMZdWfMOD5VvkrjJYTI.woff2
│ │ ├── va9C4kDNxMZdWfMOD5VvkrjKYTLHdQ.woff2
│ │ ├── va9C4kDNxMZdWfMOD5VvkrjNYTLHdQ.woff2
│ │ ├── va9E4kDNxMZdWfMOD5Vvk4jLeTY.woff2
│ │ ├── va9E4kDNxMZdWfMOD5Vvl4jL.woff2
│ │ ├── va9E4kDNxMZdWfMOD5VvlIjLeTY.woff2
│ │ ├── va9E4kDNxMZdWfMOD5Vvm4jLeTY.woff2
│ │ ├── va9E4kDNxMZdWfMOD5VvmIjLeTY.woff2
│ │ ├── va9E4kDNxMZdWfMOD5VvmYjLeTY.woff2
│ │ ├── va9E4kDNxMZdWfMOD5VvmojLeTY.woff2
│ │ ├── va9f4kDNxMZdWfMOD5VvkrByRCf0VFn2lg.woff2
│ │ ├── va9f4kDNxMZdWfMOD5VvkrByRCf1VFn2lg.woff2
│ │ ├── va9f4kDNxMZdWfMOD5VvkrByRCf2VFn2lg.woff2
│ │ ├── va9f4kDNxMZdWfMOD5VvkrByRCf3VFn2lg.woff2
│ │ ├── va9f4kDNxMZdWfMOD5VvkrByRCf4VFk.woff2
│ │ ├── va9f4kDNxMZdWfMOD5VvkrByRCf7VFn2lg.woff2
│ │ └── va9f4kDNxMZdWfMOD5VvkrByRCf8VFn2lg.woff2
│ ├── lora
│ │ ├── 0QIhMX1D_JOuMw_LIftL.woff2
│ │ ├── 0QIhMX1D_JOuMw_LJftLp_A.woff2
│ │ ├── 0QIhMX1D_JOuMw_LLPtLp_A.woff2
│ │ ├── 0QIhMX1D_JOuMw_LL_tLp_A.woff2
│ │ ├── 0QIhMX1D_JOuMw_LLvtLp_A.woff2
│ │ ├── 0QIvMX1D_JOuMw77I-NP.woff2
│ │ ├── 0QIvMX1D_JOuMwT7I-NP.woff2
│ │ ├── 0QIvMX1D_JOuMwX7I-NP.woff2
│ │ ├── 0QIvMX1D_JOuMwf7I-NP.woff2
│ │ ├── 0QIvMX1D_JOuMwr7Iw.woff2
│ │ ├── index.css
│ │ └── index.js
│ ├── merriweather
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── u-440qyriQwlOrhSvowK_l5-cSZMZ-Y.woff2
│ │ ├── u-440qyriQwlOrhSvowK_l5-ciZMZ-Y.woff2
│ │ ├── u-440qyriQwlOrhSvowK_l5-cyZMZ-Y.woff2
│ │ ├── u-440qyriQwlOrhSvowK_l5-eCZMZ-Y.woff2
│ │ ├── u-440qyriQwlOrhSvowK_l5-fCZM.woff2
│ │ ├── u-4l0qyriQwlOrhSvowK_l5-eR71Wvf1jvzRPA.woff2
│ │ ├── u-4l0qyriQwlOrhSvowK_l5-eR71Wvf2jvzRPA.woff2
│ │ ├── u-4l0qyriQwlOrhSvowK_l5-eR71Wvf3jvzRPA.woff2
│ │ ├── u-4l0qyriQwlOrhSvowK_l5-eR71Wvf4jvw.woff2
│ │ ├── u-4l0qyriQwlOrhSvowK_l5-eR71Wvf8jvzRPA.woff2
│ │ ├── u-4m0qyriQwlOrhSvowK_l5-eRZAf-LHrw.woff2
│ │ ├── u-4m0qyriQwlOrhSvowK_l5-eRZBf-LHrw.woff2
│ │ ├── u-4m0qyriQwlOrhSvowK_l5-eRZDf-LHrw.woff2
│ │ ├── u-4m0qyriQwlOrhSvowK_l5-eRZKf-LHrw.woff2
│ │ ├── u-4m0qyriQwlOrhSvowK_l5-eRZOf-I.woff2
│ │ ├── u-4n0qyriQwlOrhSvowK_l52xwNZV8f6lvg.woff2
│ │ ├── u-4n0qyriQwlOrhSvowK_l52xwNZVcf6lvg.woff2
│ │ ├── u-4n0qyriQwlOrhSvowK_l52xwNZVsf6lvg.woff2
│ │ ├── u-4n0qyriQwlOrhSvowK_l52xwNZWMf6.woff2
│ │ └── u-4n0qyriQwlOrhSvowK_l52xwNZXMf6lvg.woff2
│ └── opendyslexic
│ │ ├── OpenDyslexic-Bold-Italic.woff2
│ │ ├── OpenDyslexic-Bold.woff2
│ │ ├── OpenDyslexic-Italic.woff2
│ │ ├── OpenDyslexic-Regular.woff2
│ │ ├── index.css
│ │ └── index.js
├── styles
│ └── custom.css
└── themes
│ ├── dark.json
│ ├── light.json
│ └── sepia.json
├── root
├── game
│ ├── intercept.ink
│ └── showcase.ink
└── logo.png
├── src
├── app.css
├── atrament
│ ├── hooks.js
│ ├── init.js
│ ├── load-defaults.js
│ ├── markup.js
│ ├── on-continue-story.js
│ ├── on-game-start.js
│ ├── scene-processors.js
│ └── settings-handlers.js
├── components
│ ├── app.jsx
│ ├── markup
│ │ ├── banner
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── block
│ │ │ └── index.jsx
│ │ ├── button
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── css
│ │ │ └── index.jsx
│ │ ├── font
│ │ │ └── index.jsx
│ │ ├── highlight
│ │ │ └── index.jsx
│ │ ├── html-fragment.jsx
│ │ ├── index.js
│ │ ├── info
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── inline-image
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── input
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── link
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── picture
│ │ │ └── index.jsx
│ │ ├── progress
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── spoiler
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── table
│ │ │ └── index.jsx
│ │ ├── url
│ │ │ └── index.jsx
│ │ └── video
│ │ │ └── index.jsx
│ ├── menu
│ │ ├── index.jsx
│ │ └── index.module.css
│ ├── router.jsx
│ ├── routes
│ │ ├── about.jsx
│ │ ├── game.jsx
│ │ └── home.jsx
│ ├── ui
│ │ ├── animation-grid
│ │ │ ├── index.jsx
│ │ │ └── style.module.css
│ │ ├── application-wrapper
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── backdrop
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── block
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── break
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── close-button
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── collapse
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── container-flex
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── container-image
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── container-modal
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── container-text
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── container
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── error-modal
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── header
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── icons
│ │ │ └── index.jsx
│ │ ├── link-home
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── link-menu
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── loading
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── markup
│ │ │ └── index.jsx
│ │ ├── menu-list-item
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── modal
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── table
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── tabs
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── text-paragraph
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ └── toggle
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ └── views
│ │ ├── about
│ │ ├── atrament-logo.png
│ │ ├── iftf-logo.png
│ │ ├── index.jsx
│ │ └── index.module.css
│ │ ├── debugger
│ │ ├── functions.jsx
│ │ ├── globaltags.jsx
│ │ ├── goto.jsx
│ │ ├── index.jsx
│ │ ├── index.module.css
│ │ ├── info.jsx
│ │ ├── var-editor.jsx
│ │ ├── var-editor.module.css
│ │ ├── variables.jsx
│ │ └── visits.jsx
│ │ ├── home-menu
│ │ ├── game-cover.jsx
│ │ ├── index.jsx
│ │ ├── load-game.jsx
│ │ └── use-game-controls.js
│ │ ├── loadgame
│ │ └── index.jsx
│ │ ├── overlay
│ │ ├── index.jsx
│ │ └── index.module.css
│ │ ├── savegame
│ │ └── index.jsx
│ │ ├── sessions
│ │ └── index.jsx
│ │ ├── settings
│ │ ├── index.jsx
│ │ ├── index.module.css
│ │ ├── settings-animation
│ │ │ └── index.jsx
│ │ ├── settings-font
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── settings-fullscreen
│ │ │ └── index.jsx
│ │ ├── settings-sound
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── settings-text
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ └── settings-theme
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── story-error
│ │ └── index.jsx
│ │ ├── story
│ │ ├── choice-button-group
│ │ │ └── index.jsx
│ │ ├── choice-button
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── choices
│ │ │ └── index.jsx
│ │ ├── click-to-continue
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── container-choices
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── container-scenes
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ ├── index.jsx
│ │ ├── scene-paragraph
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ └── scene
│ │ │ ├── index.jsx
│ │ │ └── index.module.css
│ │ └── toolbar
│ │ ├── index.jsx
│ │ └── index.module.css
├── constants.js
├── context.js
├── fonts.js
├── i18n.json
├── index.jsx
├── themes.js
└── utils
│ ├── array-shuffle.js
│ ├── get-tag-attributes.js
│ ├── mute-when-inactive.js
│ ├── neutralino-out-of-bounds-fix.js
│ ├── page-background.js
│ └── preload-images.js
├── tools
├── ink-compile.cjs
├── install-inklecate.cjs
├── neutralino-postbuild.mjs
└── neutralino-prepare.cjs
├── vite.config.js
└── vite
├── ink-compiler-plugin.js
├── pwa-assets.config.js
├── pwa-config.js
└── remove-ink-files-plugin.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "preact"
4 | ],
5 | "parser": "@babel/eslint-parser",
6 | "parserOptions": {
7 | "requireConfigFile": false
8 | },
9 | "rules": {
10 | "linebreak-style": ["error", "unix"],
11 | "brace-style": ["error", "1tbs"],
12 | "indent": ["error", 2],
13 | "react/jsx-indent-props": ["error", 2],
14 | "react/no-danger": [0]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | package-lock.json
11 | node_modules
12 | build
13 | build-ssr
14 | *.local
15 |
16 | # compiled ink
17 | root/**/*.js
18 | root/**/*.json
19 |
20 | # inklecate
21 | tools/inklecate*
22 |
23 | # neutralino
24 | tools/neutralino/*
25 |
26 | # Editor directories and files
27 | .vscode/*
28 | !.vscode/extensions.json
29 | .idea
30 | .DS_Store
31 | *.suo
32 | *.ntvs*
33 | *.njsproj
34 | *.sln
35 | *.sw?
36 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 Serhii "techniX" Mozhaiskyi
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/atrament.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Atrament Web UI demo",
3 | "short_name": "atrament-web-ui",
4 | "description": "Atrament Web UI - shell for Ink games",
5 | "theme": "light",
6 | "font": "System",
7 | "language": "en",
8 | "locale": "en-US",
9 | "game": {
10 | "path": "game",
11 | "source": "intercept.ink"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= title %>
6 |
7 |
8 |
9 |
10 |
11 | <%- neutralino %>
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "noEmit": true,
7 | "allowJs": true,
8 | "checkJs": true,
9 |
10 | /* Preact Config */
11 | "jsx": "react-jsx",
12 | "jsxImportSource": "preact",
13 | "skipLibCheck": true,
14 | "paths": {
15 | "react": ["./node_modules/preact/compat/"],
16 | "react-dom": ["./node_modules/preact/compat/"],
17 | "src/*": ["./src/*"],
18 | },
19 |
20 | // "baseUrl": "."
21 | },
22 | "include": ["node_modules/vite/client.d.ts", "**/*"]
23 | }
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@atrament/web-ui",
3 | "version": "2.2.0",
4 | "license": "MIT",
5 | "type": "module",
6 | "scripts": {
7 | "start": "vite",
8 | "build": "vite build",
9 | "build-web": "vite build",
10 | "build-singlefile": "vite build --mode singlefile",
11 | "build-standalone": "vite build --mode standalone && node tools/neutralino-prepare.cjs && npm run neutralino",
12 | "preview": "vite preview",
13 | "lint": "eslint --ext .jsx --ext js src",
14 | "compile-ink-script": "node tools/ink-compile.cjs",
15 | "install-inklecate": "node tools/install-inklecate.cjs",
16 | "neutralino": "cd build/.tmp_neutralino && neu build && node-rmrf ../standalone && move-file dist ../standalone && cd .. && node-rmrf ./.tmp_neutralino && cd ./standalone && node ../../tools/neutralino-postbuild.mjs"
17 | },
18 | "dependencies": {
19 | "@atrament/web": "2.1.1",
20 | "@eo-locale/preact": "1.7.2",
21 | "@nanostores/preact": "0.5.2",
22 | "history": "5.3.0",
23 | "inkjs": "2.3.2",
24 | "normalize.css": "8.0.1",
25 | "preact": "10.26.5",
26 | "preact-router": "4.1.2",
27 | "seamless-scroll-polyfill": "2.3.4"
28 | },
29 | "devDependencies": {
30 | "@neutralinojs/neu": "11.4.0",
31 | "@preact/preset-vite": "2.10.1",
32 | "@terascope/fetch-github-release": "2.1.0",
33 | "@vite-pwa/assets-generator": "1.0.0",
34 | "eslint": "8.57.0",
35 | "eslint-config-preact": "1.5.0",
36 | "jest": "29.7.0",
37 | "move-file-cli": "3.0.0",
38 | "node-rmrf": "1.2.0",
39 | "recursive-copy": "2.0.14",
40 | "vite": "6.2.6",
41 | "vite-plugin-clean-build": "1.4.1",
42 | "vite-plugin-html": "3.2.2",
43 | "vite-plugin-pwa": "1.0.0",
44 | "vite-plugin-singlefile": "2.2.0",
45 | "vite-plugin-zip-pack": "1.2.4",
46 | "zip-a-folder": "^3.1.9"
47 | },
48 | "eslintConfig": {
49 | "extends": "preact"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/index.js:
--------------------------------------------------------------------------------
1 | import('./index.css');
2 |
3 | export default {
4 | name: 'Fira Sans',
5 | fallback: 'sans-serif',
6 | };
7 |
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eQhf6TF0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eQhf6TF0.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eRRf6TF0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eRRf6TF0.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eRhf6.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eRhf6.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eSBf6TF0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eSBf6TF0.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eSRf6TF0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eSRf6TF0.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eShf6TF0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eShf6TF0.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eSxf6TF0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9B4kDNxMZdWfMOD5VnLK3eSxf6TF0.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjEYTLHdQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjEYTLHdQ.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjFYTLHdQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjFYTLHdQ.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjGYTLHdQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjGYTLHdQ.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjHYTLHdQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjHYTLHdQ.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjJYTI.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjJYTI.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjKYTLHdQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjKYTLHdQ.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjNYTLHdQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9C4kDNxMZdWfMOD5VvkrjNYTLHdQ.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvk4jLeTY.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvk4jLeTY.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvl4jL.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvl4jL.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvlIjLeTY.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvlIjLeTY.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvm4jLeTY.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvm4jLeTY.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmIjLeTY.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmIjLeTY.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmYjLeTY.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmYjLeTY.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmojLeTY.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmojLeTY.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf0VFn2lg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf0VFn2lg.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf1VFn2lg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf1VFn2lg.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf2VFn2lg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf2VFn2lg.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf3VFn2lg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf3VFn2lg.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf4VFk.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf4VFk.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf7VFn2lg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf7VFn2lg.woff2
--------------------------------------------------------------------------------
/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf8VFn2lg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/fira-sans/va9f4kDNxMZdWfMOD5VvkrByRCf8VFn2lg.woff2
--------------------------------------------------------------------------------
/resources/fonts/lora/0QIhMX1D_JOuMw_LIftL.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/lora/0QIhMX1D_JOuMw_LIftL.woff2
--------------------------------------------------------------------------------
/resources/fonts/lora/0QIhMX1D_JOuMw_LJftLp_A.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/lora/0QIhMX1D_JOuMw_LJftLp_A.woff2
--------------------------------------------------------------------------------
/resources/fonts/lora/0QIhMX1D_JOuMw_LLPtLp_A.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/lora/0QIhMX1D_JOuMw_LLPtLp_A.woff2
--------------------------------------------------------------------------------
/resources/fonts/lora/0QIhMX1D_JOuMw_LL_tLp_A.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/lora/0QIhMX1D_JOuMw_LL_tLp_A.woff2
--------------------------------------------------------------------------------
/resources/fonts/lora/0QIhMX1D_JOuMw_LLvtLp_A.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/lora/0QIhMX1D_JOuMw_LLvtLp_A.woff2
--------------------------------------------------------------------------------
/resources/fonts/lora/0QIvMX1D_JOuMw77I-NP.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/lora/0QIvMX1D_JOuMw77I-NP.woff2
--------------------------------------------------------------------------------
/resources/fonts/lora/0QIvMX1D_JOuMwT7I-NP.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/lora/0QIvMX1D_JOuMwT7I-NP.woff2
--------------------------------------------------------------------------------
/resources/fonts/lora/0QIvMX1D_JOuMwX7I-NP.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/lora/0QIvMX1D_JOuMwX7I-NP.woff2
--------------------------------------------------------------------------------
/resources/fonts/lora/0QIvMX1D_JOuMwf7I-NP.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/lora/0QIvMX1D_JOuMwf7I-NP.woff2
--------------------------------------------------------------------------------
/resources/fonts/lora/0QIvMX1D_JOuMwr7Iw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/lora/0QIvMX1D_JOuMwr7Iw.woff2
--------------------------------------------------------------------------------
/resources/fonts/lora/index.js:
--------------------------------------------------------------------------------
1 | import('./index.css');
2 |
3 | export default {
4 | name: 'Lora',
5 | fallback: 'serif',
6 | };
7 |
--------------------------------------------------------------------------------
/resources/fonts/merriweather/index.js:
--------------------------------------------------------------------------------
1 | import('./index.css');
2 |
3 | export default {
4 | name: 'Merriweather',
5 | fallback: 'serif',
6 | };
7 |
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-440qyriQwlOrhSvowK_l5-cSZMZ-Y.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-440qyriQwlOrhSvowK_l5-cSZMZ-Y.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-440qyriQwlOrhSvowK_l5-ciZMZ-Y.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-440qyriQwlOrhSvowK_l5-ciZMZ-Y.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-440qyriQwlOrhSvowK_l5-cyZMZ-Y.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-440qyriQwlOrhSvowK_l5-cyZMZ-Y.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-440qyriQwlOrhSvowK_l5-eCZMZ-Y.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-440qyriQwlOrhSvowK_l5-eCZMZ-Y.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-440qyriQwlOrhSvowK_l5-fCZM.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-440qyriQwlOrhSvowK_l5-fCZM.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4l0qyriQwlOrhSvowK_l5-eR71Wvf1jvzRPA.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4l0qyriQwlOrhSvowK_l5-eR71Wvf1jvzRPA.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4l0qyriQwlOrhSvowK_l5-eR71Wvf2jvzRPA.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4l0qyriQwlOrhSvowK_l5-eR71Wvf2jvzRPA.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4l0qyriQwlOrhSvowK_l5-eR71Wvf3jvzRPA.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4l0qyriQwlOrhSvowK_l5-eR71Wvf3jvzRPA.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4l0qyriQwlOrhSvowK_l5-eR71Wvf4jvw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4l0qyriQwlOrhSvowK_l5-eR71Wvf4jvw.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4l0qyriQwlOrhSvowK_l5-eR71Wvf8jvzRPA.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4l0qyriQwlOrhSvowK_l5-eR71Wvf8jvzRPA.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4m0qyriQwlOrhSvowK_l5-eRZAf-LHrw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4m0qyriQwlOrhSvowK_l5-eRZAf-LHrw.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4m0qyriQwlOrhSvowK_l5-eRZBf-LHrw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4m0qyriQwlOrhSvowK_l5-eRZBf-LHrw.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4m0qyriQwlOrhSvowK_l5-eRZDf-LHrw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4m0qyriQwlOrhSvowK_l5-eRZDf-LHrw.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4m0qyriQwlOrhSvowK_l5-eRZKf-LHrw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4m0qyriQwlOrhSvowK_l5-eRZKf-LHrw.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4m0qyriQwlOrhSvowK_l5-eRZOf-I.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4m0qyriQwlOrhSvowK_l5-eRZOf-I.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4n0qyriQwlOrhSvowK_l52xwNZV8f6lvg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4n0qyriQwlOrhSvowK_l52xwNZV8f6lvg.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4n0qyriQwlOrhSvowK_l52xwNZVcf6lvg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4n0qyriQwlOrhSvowK_l52xwNZVcf6lvg.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4n0qyriQwlOrhSvowK_l52xwNZVsf6lvg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4n0qyriQwlOrhSvowK_l52xwNZVsf6lvg.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4n0qyriQwlOrhSvowK_l52xwNZWMf6.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4n0qyriQwlOrhSvowK_l52xwNZWMf6.woff2
--------------------------------------------------------------------------------
/resources/fonts/merriweather/u-4n0qyriQwlOrhSvowK_l52xwNZXMf6lvg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/merriweather/u-4n0qyriQwlOrhSvowK_l52xwNZXMf6lvg.woff2
--------------------------------------------------------------------------------
/resources/fonts/opendyslexic/OpenDyslexic-Bold-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/opendyslexic/OpenDyslexic-Bold-Italic.woff2
--------------------------------------------------------------------------------
/resources/fonts/opendyslexic/OpenDyslexic-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/opendyslexic/OpenDyslexic-Bold.woff2
--------------------------------------------------------------------------------
/resources/fonts/opendyslexic/OpenDyslexic-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/opendyslexic/OpenDyslexic-Italic.woff2
--------------------------------------------------------------------------------
/resources/fonts/opendyslexic/OpenDyslexic-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/resources/fonts/opendyslexic/OpenDyslexic-Regular.woff2
--------------------------------------------------------------------------------
/resources/fonts/opendyslexic/index.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'OpenDyslexic';
3 | font-display: swap;
4 | font-style: normal;
5 | font-weight: 400;
6 | src: url(OpenDyslexic-Regular.woff2) format('woff2');
7 | }
8 |
9 | @font-face {
10 | font-family: 'OpenDyslexic';
11 | font-display: swap;
12 | font-style: italic;
13 | font-weight: 400;
14 | src: url(OpenDyslexic-Italic.woff2) format('woff2');
15 | }
16 |
17 | @font-face {
18 | font-family: 'OpenDyslexic';
19 | font-display: swap;
20 | font-style: normal;
21 | font-weight: 700;
22 | src: url(OpenDyslexic-Bold.woff2) format('woff2');
23 | }
24 |
25 | @font-face {
26 | font-family: 'OpenDyslexic';
27 | font-display: swap;
28 | font-style: italic;
29 | font-weight: 700;
30 | src: url(OpenDyslexic-Bold-Italic.woff2) format('woff2');
31 | }
32 |
--------------------------------------------------------------------------------
/resources/fonts/opendyslexic/index.js:
--------------------------------------------------------------------------------
1 | import('./index.css');
2 |
3 | export default {
4 | name: 'OpenDyslexic',
5 | fallback: 'serif',
6 | };
7 |
--------------------------------------------------------------------------------
/resources/styles/custom.css:
--------------------------------------------------------------------------------
1 | /* Write custom CSS classes and rules here */
2 |
3 | /*
4 | Available classes:
5 | .atrament-ui-app Application wrapper div
6 | .atrament-backdrop Modal backdrop
7 | .atrament-block Block
8 | .atrament-container Application container div
9 | .atrament-flex-container Application flex container div
10 | .atrament-image-container Image container div
11 | .atrament-image Image inside of a container
12 | .atrament-text-container Text container div
13 | .atrament-header Header
14 | .atrament-loading Loading animation div
15 | .atrament-modal Modal div
16 | .atrament-overlay Overlay div
17 | .atrament-toolbar Toolbar div
18 | .atrament-container-choices Container for choices list
19 | .atrament-container-scene Scene container div
20 | .atrament-scene Scene content div
21 | */
22 |
--------------------------------------------------------------------------------
/resources/themes/dark.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dark",
3 | "theme": {
4 | "bg-color": "#4c3b4d",
5 | "fg-color": "#FCFCFC",
6 | "shade-color": "rgba(255, 255, 255, 0.1)",
7 | "font-color": "#EEEEEE",
8 | "accent-bg-color": "#EEEEEE",
9 | "accent-fg-color": "#F7567C",
10 | "border-radius": "0.5rem",
11 | "border-radius-inline": "0.25rem"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/resources/themes/light.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "light",
3 | "theme": {
4 | "bg-color": "#FCFCFC",
5 | "fg-color": "#5D576B",
6 | "shade-color": "rgba(0, 0, 0, 0.1)",
7 | "font-color": "#333333",
8 | "accent-bg-color": "#FCFCFC",
9 | "accent-fg-color": "#F7567C",
10 | "border-radius": "0.5rem",
11 | "border-radius-inline": "0.25rem"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/resources/themes/sepia.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sepia",
3 | "theme": {
4 | "bg-color": "#ffeedb",
5 | "fg-color": "#4c3b4d",
6 | "shade-color": "rgba(0, 0, 0, 0.1)",
7 | "font-color": "#333333",
8 | "accent-bg-color": "#ffeedb",
9 | "accent-fg-color": "#a53850",
10 | "border-radius": "0.5rem",
11 | "border-radius-inline": "0.25rem"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/root/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/root/logo.png
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg-color: #FCFCFC;
3 | --fg-color: #5D576B;
4 | --shade-color: rgba(0, 0, 0, 0.1);
5 | --font-color: #333333;
6 | --accent-bg-color: #FCFCFC;
7 | --accent-fg-color: #F7567C;
8 | --border-radius: 0.5rem;
9 | --border-radius-inline: 0.25rem;
10 | --font-interface: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
11 | --game-font-size: 100%;
12 | }
13 |
14 | html {
15 | scroll-behavior: smooth;
16 | }
17 |
18 | body {
19 | margin: 0px;
20 | padding: 0px;
21 | background-color: var(--bg-color);
22 | color: var(--font-color);
23 | font-size: 1.2em;
24 | font-family: var(--font-interface);
25 | }
26 |
27 | * {
28 | box-sizing: border-box;
29 | }
30 |
31 | mark {
32 | background-color: inherit;
33 | color: var(--accent-fg-color);
34 | }
35 |
36 | blockquote {
37 | border-left: 0.2em solid var(--accent-fg-color);
38 | margin-left: 0;
39 | padding-left: 1em;
40 | }
41 |
42 | h1, h2, h3, h4, h4, h6 {
43 | line-height: normal;
44 | }
45 |
46 | @keyframes appear {
47 | 0% {
48 | opacity: 0;
49 | }
50 | 100% {
51 | opacity: 1;
52 | }
53 | }
54 |
55 | a, a:visited {
56 | color: var(--accent-fg-color);
57 | }
58 |
59 | .animation_appear {
60 | animation-name: appear;
61 | animation-duration: var(--animation-disabled, 0.5s);
62 | animation-timing-function: ease-in;
63 | animation-delay: 0s;
64 | animation-iteration-count: 1;
65 | animation-fill-mode: forwards;
66 | }
67 |
68 | /* scrollbars */
69 |
70 | *::-webkit-scrollbar {
71 | width: 16px;
72 | }
73 |
74 | *::-webkit-scrollbar-track {
75 | border-radius: 8px;
76 | }
77 |
78 | *::-webkit-scrollbar-thumb {
79 | height: 56px;
80 | border-radius: 8px;
81 | border: 4px solid transparent;
82 | background-clip: content-box;
83 | background-color: #888;
84 | }
85 |
86 | *::-webkit-scrollbar-thumb:hover {
87 | background-color: #555;
88 | }
89 |
--------------------------------------------------------------------------------
/src/atrament/hooks.js:
--------------------------------------------------------------------------------
1 | import { useContext, useCallback } from 'preact/hooks';
2 | import { useStore } from '@nanostores/preact';
3 | import { AtramentContext } from 'src/context';
4 | import { OVERLAY_STORE_KEY } from 'src/constants';
5 |
6 | export const useAtrament = () => {
7 | const atrament = useContext(AtramentContext);
8 |
9 | const inkErrorWrapper = useCallback((fn) => {
10 | try {
11 | return fn();
12 | } catch (e) {
13 | if (atrament.ink.story().onError) {
14 | atrament.ink.story().onError(e.toString());
15 | }
16 | return null;
17 | }
18 | }, [ atrament ]);
19 |
20 | const getAssetPath = useCallback(
21 | (file) => file ? atrament.game.getAssetPath(file) : null,
22 | [ atrament ]
23 | );
24 |
25 | const updateSettings = useCallback(
26 | (name, value) => {
27 | atrament.settings.set(name, value);
28 | atrament.settings.save();
29 | },
30 | [ atrament ]
31 | );
32 |
33 | const setStateSubkey = useCallback(
34 | (...args) => atrament.state.setSubkey(...args),
35 | [ atrament ]
36 | );
37 |
38 | const evaluateInkFunction = useCallback(
39 | (fn, args=[]) => {
40 | let result = {};
41 | try {
42 | result = atrament.ink.evaluateFunction(fn, args, true);
43 | } catch (e) {
44 | if (atrament.ink.story().onError) {
45 | atrament.ink.story().onError(e.toString());
46 | }
47 | result.error = e.toString();
48 | }
49 | return result;
50 | },
51 | [ atrament ]
52 | );
53 |
54 | const setInkVariable = useCallback(
55 | (...args) => inkErrorWrapper(() => atrament.ink.setVariable(...args)),
56 | [ atrament, inkErrorWrapper ]
57 | );
58 |
59 | const getInkVariable = useCallback(
60 | (...args) => inkErrorWrapper(() => atrament.ink.getVariable(...args)),
61 | [ atrament, inkErrorWrapper ]
62 | );
63 |
64 | const makeChoice = useCallback(
65 | (...args) => inkErrorWrapper(() => atrament.game.makeChoice(...args)),
66 | [ atrament, inkErrorWrapper ]
67 | );
68 |
69 | const continueStory = useCallback(
70 | (...args) => inkErrorWrapper(() => atrament.game.continueStory(...args)),
71 | [ atrament, inkErrorWrapper ]
72 | );
73 |
74 | const resetBackground = useCallback(() => {
75 | atrament.state.setSubkey('game', 'background', null);
76 | atrament.state.setSubkey('game', 'background_page', null);
77 | }, [ atrament ]);
78 |
79 | return {
80 | atrament,
81 | canResume: atrament.game.canResume,
82 | gameStart: atrament.game.start,
83 | gameResume: atrament.game.resume,
84 | makeChoice,
85 | continueStory,
86 | getAssetPath,
87 | updateSettings,
88 | setStateSubkey,
89 | evaluateInkFunction,
90 | setInkVariable,
91 | getInkVariable,
92 | resetBackground
93 | };
94 | };
95 |
96 | export const useAtramentState = (keys = undefined) => {
97 | const atrament = useContext(AtramentContext);
98 | return useStore(atrament.store, {keys});
99 | };
100 |
101 | export const useAtramentOverlay = () => {
102 | const { evaluateInkFunction, setStateSubkey } = useAtrament();
103 | const atramentState = useAtramentState([OVERLAY_STORE_KEY]);
104 |
105 | const setOverlayContent = useCallback((overlayName, content) => {
106 | setStateSubkey(OVERLAY_STORE_KEY, 'current', overlayName);
107 | let textContent = content;
108 | const contentArray = content.split('\n');
109 | const firstLine = contentArray.shift();
110 | const title = firstLine.match(/\[title\](.+?)\[\/title\]/i);
111 | if (title) {
112 | setStateSubkey(OVERLAY_STORE_KEY, 'title', title[1]);
113 | textContent = contentArray.join('\n');
114 | }
115 | setStateSubkey(OVERLAY_STORE_KEY, 'content', textContent);
116 | }, [ setStateSubkey ]);
117 |
118 | const refreshOverlay = useCallback(() => {
119 | const currentOverlay = atramentState[OVERLAY_STORE_KEY].current;
120 | if (currentOverlay) {
121 | // refresh active overlay
122 | const result = evaluateInkFunction(currentOverlay);
123 | setOverlayContent(currentOverlay, result.output);
124 | }
125 | }, [ atramentState, setOverlayContent, evaluateInkFunction ]);
126 |
127 | const closeOverlay = useCallback(() => {
128 | setStateSubkey(OVERLAY_STORE_KEY, 'current', null);
129 | setStateSubkey(OVERLAY_STORE_KEY, 'content', '');
130 | setStateSubkey(OVERLAY_STORE_KEY, 'title', null);
131 | }, [ setStateSubkey ]);
132 |
133 | return {
134 | refreshOverlay,
135 | closeOverlay,
136 | setOverlayContent,
137 | overlay: {
138 | current: atramentState[OVERLAY_STORE_KEY].current,
139 | content: atramentState[OVERLAY_STORE_KEY].content.split('\n'),
140 | title: atramentState[OVERLAY_STORE_KEY].title,
141 | }
142 | }
143 | };
144 |
--------------------------------------------------------------------------------
/src/atrament/init.js:
--------------------------------------------------------------------------------
1 |
2 | import { applicationID, gameFile, gamePath } from 'src/constants';
3 |
4 | import muteWhenInactive from 'src/utils/mute-when-inactive';
5 |
6 | import { loadDefaultFont, loadDefaultTheme } from 'src/atrament/load-defaults';
7 | import { registerSettingsHandlers } from 'src/atrament/settings-handlers'
8 | import registerSceneProcessors from 'src/atrament/scene-processors';
9 |
10 | import onGameStart from 'src/atrament/on-game-start';
11 | import onContinueStory from 'src/atrament/on-continue-story';
12 |
13 | export default async function atramentInit(atrament, Story) {
14 | // show all events in console
15 | atrament.on('*', (event, message) => console.log(
16 | `%c Atrament > ${event} `, 'color: #111111; background-color: #7FDBFF;',
17 | message
18 | ));
19 | // handle settings
20 | registerSettingsHandlers(atrament);
21 | // initialize Atrament
22 | await atrament.init(Story, {
23 | applicationID,
24 | settings: {
25 | fullscreen: false,
26 | animation: true,
27 | mute: false,
28 | volume: 50,
29 | fontSize: 100
30 | }
31 | });
32 | atrament.on('game/start', () => onGameStart(atrament));
33 | atrament.on('game/continueStory', () => onContinueStory(atrament));
34 | // initialize game
35 | await atrament.game.init(gamePath, gameFile);
36 | await atrament.game.initInkStory();
37 | // load defaults
38 | loadDefaultTheme(atrament);
39 | loadDefaultFont(atrament);
40 | // register scene processors
41 | registerSceneProcessors(atrament);
42 | // mute when tab is inactive
43 | muteWhenInactive(atrament);
44 | // set window title
45 | const metadata = atrament.state.get().metadata;
46 | if (metadata.title) {
47 | atrament.interfaces.platform.setTitle(metadata.title);
48 | }
49 | // done
50 | }
51 |
--------------------------------------------------------------------------------
/src/atrament/load-defaults.js:
--------------------------------------------------------------------------------
1 | import { gameDefaultTheme, gameDefaultFont } from 'src/constants';
2 |
3 | export function loadDefaultTheme(atrament) {
4 | // set initial theme from game
5 | const defaultTheme = atrament.state.get().metadata.theme || gameDefaultTheme;
6 | if (!atrament.settings.get('theme')) {
7 | atrament.settings.set('theme', defaultTheme);
8 | }
9 | }
10 |
11 | export function loadDefaultFont(atrament) {
12 | // set initial font from game
13 | const defaultFont = atrament.state.get().metadata.font || gameDefaultFont;
14 | if (!atrament.settings.get('font')) {
15 | atrament.settings.set('font', defaultFont);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/atrament/markup.js:
--------------------------------------------------------------------------------
1 | import MarkupComponents from 'src/components/markup';
2 | import HTMLFragment from 'src/components/markup/html-fragment';
3 |
4 | const containsHTML = (str) => /<\/?[a-z][\s\S]*>/i.test(str);
5 |
6 | function replaceWithComponent(text, regexp, replacer) {
7 | if (typeof text !== 'string') {
8 | return text;
9 | }
10 | const mentions = text.match(regexp);
11 | if (!mentions) {
12 | return text;
13 | }
14 | const splitted = text.split(regexp);
15 | const result = splitted.flatMap((fragment, index) => {
16 | return (index < mentions.length
17 | ? [fragment, replacer(mentions[index], markup)]
18 | : [fragment]);
19 | });
20 | return result;
21 | }
22 |
23 | export default function markup(text) {
24 | let processedText = [text];
25 | // find matched markup and its length
26 | const processingQueue = [];
27 | MarkupComponents.forEach(component => {
28 | const mentions = text.match(component.regexp);
29 | if (mentions) {
30 | mentions.forEach((m) => processingQueue.push({
31 | component,
32 | size: m.length
33 | }));
34 | }
35 | });
36 | // start from the longest markup elements, which may contain others
37 | processingQueue
38 | .sort((a, b) => b.size - a.size)
39 | .forEach(({ component }) => {
40 | processedText = processedText.flatMap(
41 | (item) => replaceWithComponent(
42 | item,
43 | component.regexp,
44 | component.replacer
45 | )
46 | );
47 | });
48 | return processedText.map((item, index) =>
49 | typeof item === 'string' && containsHTML(item)
50 | ? HTMLFragment({index, item})
51 | : item
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/atrament/on-continue-story.js:
--------------------------------------------------------------------------------
1 | export default function onContinueStory(atrament) {
2 | // save current story path into game.$story_path for debugging purposes
3 | const path = atrament.ink.story().state.previousPathString;
4 | atrament.state.setSubkey('game', '$story_path', path);
5 | }
6 |
--------------------------------------------------------------------------------
/src/atrament/on-game-start.js:
--------------------------------------------------------------------------------
1 | import { TOOLBAR_DEFAULT, TOOLBAR_STORE_KEY, OVERLAY_STORE_KEY, ERROR_STORE_KEY } from "src/constants";
2 |
3 | function registerToolbarHandler(atrament, toolbarFunction) {
4 | const refreshToolbar = () => {
5 | let result;
6 | try {
7 | result = atrament.ink.evaluateFunction(toolbarFunction, [], true);
8 | atrament.state.setKey(TOOLBAR_STORE_KEY, result.output);
9 | } catch (e) {
10 | atrament.ink.story().onError(e.toString());
11 | }
12 | }
13 | const delayedRefreshToolbar = () => setTimeout(refreshToolbar, 0);
14 | // refresh toolbar on continueStory and function evaluation (buttons etc)
15 | atrament.on('game/continueStory', delayedRefreshToolbar);
16 | atrament.on('ink/evaluateFunction', (params) => {
17 | if (params.function !== toolbarFunction) {
18 | delayedRefreshToolbar();
19 | }
20 | });
21 | // run refresh toolbar before game starts
22 | refreshToolbar();
23 | }
24 |
25 |
26 | export default function onGameStart(atrament) {
27 | // register error handler
28 | atrament.ink.onError((error) => atrament.state.setKey(ERROR_STORE_KEY, error));
29 | // reset overlay state
30 | atrament.state.setKey(OVERLAY_STORE_KEY, {
31 | current: null,
32 | content: '',
33 | title: null
34 | });
35 | // configure toolbar
36 | const metadata = atrament.state.get().metadata;
37 | if (metadata.toolbar) {
38 | registerToolbarHandler(atrament, metadata.toolbar);
39 | } else if (metadata.title) {
40 | atrament.state.setKey(TOOLBAR_STORE_KEY, metadata.title);
41 | } else {
42 | atrament.state.setKey(TOOLBAR_STORE_KEY, TOOLBAR_DEFAULT);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/atrament/scene-processors.js:
--------------------------------------------------------------------------------
1 | // Atrament scene processors
2 | import arrayShuffle from "src/utils/array-shuffle";
3 |
4 |
5 | function sceneBackground(scene) {
6 | // BACKGROUND and PAGE_BACKGROUND can be set to false, so we check if variable is defined
7 | const background = scene.tags?.BACKGROUND;
8 | if (typeof background !== 'undefined') {
9 | scene.images.push(background);
10 | this.state.setSubkey('game', 'background', background);
11 | }
12 | const backgroundPage = scene.tags?.PAGE_BACKGROUND;
13 | if (typeof backgroundPage !== 'undefined') {
14 | scene.images.push(backgroundPage);
15 | this.state.setSubkey('game', 'background_page', backgroundPage);
16 | }
17 | }
18 |
19 | function shuffleChoices(scene) {
20 | if (scene.tags?.SHUFFLE_CHOICES) {
21 | scene.choices = arrayShuffle(scene.choices);
22 | }
23 | }
24 |
25 | export default function registerSceneProcessors(atrament) {
26 | [sceneBackground, shuffleChoices]
27 | .forEach((p) => atrament.game.defineSceneProcessor(p.bind(atrament)));
28 | }
29 |
--------------------------------------------------------------------------------
/src/atrament/settings-handlers.js:
--------------------------------------------------------------------------------
1 | import { applyTheme } from 'src/themes';
2 | import { applyFont } from 'src/fonts';
3 |
4 | export function registerSettingsHandlers(atrament) {
5 | atrament.settings.defineHandler('theme', (oldV, value) => {
6 | applyTheme(value);
7 | });
8 | atrament.settings.defineHandler('font', (oldV, value) => {
9 | applyFont(value);
10 | });
11 | atrament.settings.defineHandler('fontSize', (oldV, value) => {
12 | document.documentElement.style.setProperty('--game-font-size', `${value}%`);
13 | });
14 | atrament.settings.defineHandler('animation', (oldV, value) => {
15 | if (value) {
16 | document.documentElement.style.removeProperty('--animation-disabled');
17 | } else {
18 | document.documentElement.style.setProperty('--animation-disabled', '0s');
19 | }
20 | });
21 | atrament.settings.defineHandler('fullscreen', (oldV, value) => {
22 | atrament.interfaces.platform.setFullscreen(value, (v) => {
23 | atrament.settings.set('fullscreen', v);
24 | atrament.settings.save();
25 | });
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/app.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useEffect, useState } from 'preact/hooks';
3 | import { TranslationsProvider } from '@eo-locale/preact';
4 | import { AtramentContext } from 'src/context';
5 | import atramentInit from 'src/atrament/init';
6 | import locales from 'src/i18n.json';
7 | import { appLanguage } from 'src/constants';
8 |
9 | import ApplicationWrapper from 'src/components/ui/application-wrapper';
10 | import ErrorModal from 'src/components/ui/error-modal';
11 | import Container from 'src/components/ui/container';
12 | import Loading from 'src/components/ui/loading';
13 | import AppRouter from './router';
14 |
15 | function App() {
16 | const [ atrament, setAtrament ] = useState(null);
17 | const [ initError, setInitError ] = useState(null);
18 |
19 | useEffect(() => {
20 | const startEngine = async () => {
21 | const { default: atrament } = await import(/* webpackChunkName: "atrament" */ "@atrament/web");
22 | // import inkjs
23 | const { Story } = await import(/* webpackChunkName: "inkjs" */ "inkjs");
24 | // initialize engine
25 | try {
26 | await atramentInit(atrament, Story);
27 | // done
28 | setAtrament(atrament);
29 | } catch (e) {
30 | console.error(e);
31 | setInitError(e.message);
32 | }
33 | };
34 | // application is ready
35 | startEngine();
36 | }, []);
37 |
38 | return (
39 |
40 |
41 |
42 | {atrament
43 | ?
44 | : initError
45 | ?
46 | :
47 | }
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export default App;
55 |
--------------------------------------------------------------------------------
/src/components/markup/banner/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | // [banner style=highlight allcaps=false]text in info block[/banner]
5 |
6 | import getTagAttributes from 'src/utils/get-tag-attributes';
7 |
8 | const BannerBlock = ({children, options}) => {
9 | const classList = [
10 | style.bannerblock,
11 | options.style === 'accent' ? style.accent : '',
12 | options.allcaps ? style.allcaps : ''
13 | ].join(' ');
14 | return ({children}
);
15 | };
16 |
17 | export default {
18 | regexp: /\[banner(?:\s+[^\]]+)?\].*?\[\/banner\]/ig,
19 | replacer: (el, markup) => {
20 | const fragments = el.match(/\[banner(.*?)\](.*?)\[\/banner\]/i);
21 | const options = getTagAttributes(fragments[1]);
22 | return ({markup(fragments[2])});
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/markup/banner/index.module.css:
--------------------------------------------------------------------------------
1 | .bannerblock {
2 | padding: 1rem;
3 | font-size: 2em;
4 | text-align: center;
5 | line-height: normal;
6 | border-top: 1px solid var(--fg-color);
7 | border-bottom: 1px solid var(--fg-color);
8 | color: var(--fg-color);
9 | }
10 |
11 | .accent {
12 | border-top: 1px solid var(--accent-fg-color);
13 | border-bottom: 1px solid var(--accent-fg-color);
14 | color: var(--accent-fg-color);
15 | }
16 |
17 | .allcaps {
18 | text-transform: uppercase;
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/markup/block/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import getTagAttributes from 'src/utils/get-tag-attributes';
3 |
4 | // [block width=50% align=left]text in bar[/block]
5 |
6 | export default {
7 | regexp: /\[block(?:\s+[^\]]+)?\].+?\[\/block\]/ig,
8 | replacer: (el, markup) => {
9 | const fragments = el.match(/\[block(.*?)\](.+?)\[\/block\]/i);
10 | let options = {};
11 | if (fragments[1]) {
12 | options = getTagAttributes(fragments[1]);
13 | }
14 | const blockStyle = {
15 | display: 'inline-block',
16 | width: options.width || '100%',
17 | 'text-align': options.align || 'inherit',
18 | 'vertical-align': options.valign || 'top',
19 | };
20 | return ({markup(fragments[2])}
);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/markup/button/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useCallback } from "preact/hooks";
4 |
5 | import getTagAttributes from 'src/utils/get-tag-attributes';
6 | import { useAtrament, useAtramentOverlay } from 'src/atrament/hooks';
7 |
8 | // [button onclick=function]button text[/button]
9 |
10 | const InlineButtonComponent = ({ children, options }) => {
11 | const { evaluateInkFunction } = useAtrament();
12 | const { setOverlayContent, refreshOverlay } = useAtramentOverlay();
13 |
14 | const clickHandler = useCallback((e) => {
15 | e.stopPropagation();
16 | const inkFn = options.onclick;
17 | const result = evaluateInkFunction(inkFn);
18 | if (result.output) {
19 | setOverlayContent(inkFn, result.output);
20 | } else {
21 | refreshOverlay();
22 | }
23 | }, [ evaluateInkFunction, setOverlayContent, refreshOverlay, options.onclick ]);
24 |
25 | let buttonStyle = options.bordered === false ? style.inline_button : style.bordered_button;
26 | return (
27 |
34 | );
35 | }
36 |
37 | export default {
38 | regexp: /\[button[ =].+?\].+?\[\/button\]/ig,
39 | replacer: (el, markup) => {
40 | const fragments = el.match(/\[button([ =].+?)\](.+?)\[\/button\]/i);
41 | let attributes = fragments[1];
42 | if (attributes.startsWith('=')) {
43 | attributes = `onclick${attributes}`;
44 | }
45 | const options = getTagAttributes(attributes);
46 | return ({markup(fragments[2])});
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/markup/button/index.module.css:
--------------------------------------------------------------------------------
1 | .inline_button {
2 | background-color: var(--bg-color);
3 | color: var(--fg-color);
4 | border: 1px solid var(--bg-color);
5 | border-radius: var(--border-radius-inline);
6 | text-decoration: none;
7 | cursor: pointer;
8 | }
9 |
10 | .inline_button:hover {
11 | border: 1px solid var(--fg-color);
12 | }
13 |
14 | .inline_button:disabled {
15 | border: 1px solid var(--bg-color);
16 | opacity: 0.5;
17 | }
18 |
19 | .bordered_button {
20 | background-color: var(--bg-color);
21 | color: var(--fg-color);
22 | text-decoration: none;
23 | cursor: pointer;
24 | border: 1px solid var(--fg-color);
25 | border-radius: var(--border-radius-inline);
26 | }
27 |
28 | .bordered_button:hover {
29 | background-color: var(--fg-color);
30 | color: var(--bg-color);
31 | }
32 |
33 | .bordered_button:disabled {
34 | background-color: var(--bg-color);
35 | color: var(--fg-color);
36 | opacity: 0.5;
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/markup/css/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | import getTagAttributes from 'src/utils/get-tag-attributes';
4 |
5 | // [css class=classname style="style applied"]text with CSS class and style applied[/css]
6 |
7 | export default {
8 | regexp: /\[css(?:\s+[^\]]+)?\].+?\[\/css\]/ig,
9 | replacer: (el, markup) => {
10 | const fragments = el.match(/\[css(.*?)\](.+?)\[\/css\]/i);
11 | let options = {};
12 | if (fragments[1]) {
13 | options = getTagAttributes(fragments[1]);
14 | }
15 | return ({markup(fragments[2])});
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/markup/font/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | // [font=Fira Sans]text with font applied[/font]
4 |
5 | export default {
6 | regexp: /\[font=.+?\].+?\[\/font\]/ig,
7 | replacer: (el, markup) => {
8 | const fragments = el.match(/\[font=(.+?)\](.+?)\[\/font\]/i);
9 | return ({markup(fragments[2])});
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/markup/highlight/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import getTagAttributes from 'src/utils/get-tag-attributes';
3 |
4 | // [highlight color=black bgcolor=red]highlighted text[/highlight]
5 |
6 | export default {
7 | regexp: /\[highlight(?:\s+[^\]]+)?\].+?\[\/highlight\]/ig,
8 | replacer: (el, markup) => {
9 | const fragments = el.match(/\[highlight(.*?)\](.+?)\[\/highlight\]/i);
10 | let options = {};
11 | if (fragments[1]) {
12 | options = getTagAttributes(fragments[1]);
13 | }
14 | const highlightStyle = {
15 | 'background-color': options.bgcolor || 'var(--accent-bg-color)',
16 | color: options.color || 'var(--accent-fg-color)',
17 | padding: options.bgcolor ? '0.1em' : 'inherit',
18 | 'box-decoration-break': 'clone'
19 | };
20 | return ({markup(fragments[2])});
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/markup/html-fragment.jsx:
--------------------------------------------------------------------------------
1 | const HTMLFragment = ({index, item}) => ();
2 | export default HTMLFragment;
3 |
--------------------------------------------------------------------------------
/src/components/markup/index.js:
--------------------------------------------------------------------------------
1 | import Banner from './banner';
2 | import Block from './block';
3 | import Button from './button';
4 | import spanCSS from './css';
5 | import Font from './font';
6 | import Highlight from './highlight';
7 | import Info from './info';
8 | import InlineImage from './inline-image';
9 | import Input from './input';
10 | import InlineLink from './link';
11 | import Picture from './picture';
12 | import Progress from './progress';
13 | import Spoiler from './spoiler';
14 | import Table from './table';
15 | import webURL from './url';
16 | import Video from './video';
17 |
18 | export default [
19 | Banner,
20 | Block,
21 | Button,
22 | spanCSS,
23 | Font,
24 | Highlight,
25 | Info,
26 | InlineImage,
27 | Input,
28 | InlineLink,
29 | Picture,
30 | Progress,
31 | Spoiler,
32 | Table,
33 | webURL,
34 | Video
35 | ];
36 |
--------------------------------------------------------------------------------
/src/components/markup/info/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | // [info font=system side=highlight]text in info block[/info]
5 |
6 | import getTagAttributes from 'src/utils/get-tag-attributes';
7 |
8 | const InfoBlock = ({children, options}) => {
9 | const classList = [
10 | style.infoblock,
11 | options.font === 'system' ? style.font_ui : '',
12 | options.side === 'accent' ? style.side_accent : '',
13 | options.side === 'highlight' ? style.side_highlight : ''
14 | ].join(' ');
15 | return ({children}
);
16 | };
17 |
18 | export default {
19 | regexp: /\[info(?:\s+[^\]]+)?\].*?\[\/info\]/ig,
20 | replacer: (el, markup) => {
21 | const fragments = el.match(/\[info(.*?)\](.*?)\[\/info\]/i);
22 | const options = getTagAttributes(fragments[1]);
23 | return ({markup(fragments[2])});
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/markup/info/index.module.css:
--------------------------------------------------------------------------------
1 | .infoblock {
2 | display: block;
3 | width: 100%;
4 | background-color: var(--shade-color);
5 | padding: 0.5em;
6 | border-radius: var(--border-radius);
7 | }
8 |
9 | .font_ui {
10 | font-family: var(--font-interface);
11 | }
12 |
13 | .side_highlight {
14 | border-left: 0.3rem solid var(--fg-color);
15 | }
16 |
17 | .side_accent {
18 | border-left: 0.3rem solid var(--accent-fg-color);
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/markup/inline-image/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useAtrament } from 'src/atrament/hooks';
4 |
5 | // [img]path/to/image.jpg[/img]
6 |
7 | const InlineImage = ({ src }) => {
8 | const { getAssetPath } = useAtrament();
9 | return (
);
10 | }
11 |
12 | export default {
13 | regexp: /\[img\].+?\[\/img\]/ig,
14 | replacer: (el) => {
15 | const fragments = el.match(/\[img\](.+?)\[\/img\]/i);
16 | return ();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/markup/inline-image/index.module.css:
--------------------------------------------------------------------------------
1 | .inline_image {
2 | height: 1.5em;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/markup/input/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useContext, useEffect, useState } from 'preact/hooks';
3 | import style from './index.module.css';
4 |
5 | import getTagAttributes from 'src/utils/get-tag-attributes';
6 | import { useAtrament } from 'src/atrament/hooks';
7 | import { ActiveContentContext } from 'src/context';
8 |
9 | // [input var=variable placeholder="placeholder text" type=number]
10 |
11 | const Input = ({options}) => {
12 | const isActive = useContext(ActiveContentContext);
13 |
14 | const [ defaultValue, setDefaultValue ] = useState(null);
15 | const { getInkVariable, setInkVariable } = useAtrament();
16 | useEffect(
17 | () => setDefaultValue(getInkVariable(options.var)),
18 | [getInkVariable, options.var]
19 | );
20 | const onInput = (e) => {
21 | let targetValue = e.srcElement.value || defaultValue;
22 | if (options.type === 'number') {
23 | targetValue = +targetValue;
24 | }
25 | setInkVariable(options.var, targetValue);
26 | setDefaultValue(targetValue);
27 | };
28 | const inputType = options.type === 'number' ? 'number' : 'text';
29 | return (
30 |
31 | );
32 | };
33 |
34 | export default {
35 | regexp: /\[input .+?\]/ig,
36 | replacer: (el) => {
37 | const fragments = el.match(/\[input(.+?)\]/i);
38 | const options = getTagAttributes(fragments[1]);
39 | return ();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/markup/input/index.module.css:
--------------------------------------------------------------------------------
1 | .input {
2 | width: 100%;
3 | padding: 0.25rem;
4 | border: 2px solid var(--fg-color);
5 | border-radius: var(--border-radius-inline);
6 | background-color: var(--bg-color);
7 | color: var(--fg-color);
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/markup/link/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useCallback } from "preact/hooks";
4 | import { ERROR_STORE_KEY } from 'src/constants';
5 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
6 |
7 | // [link=target choice text]Text[/link]
8 |
9 | const InlineLink = ({ children, choice }) => {
10 | const { atrament, makeChoice, continueStory } = useAtrament();
11 | const atramentState = useAtramentState(['scenes']);
12 |
13 | const clickHandler = useCallback(() => {
14 | const lastSceneIndex = atramentState.scenes.length - 1;
15 | const currentScene = atramentState.scenes[lastSceneIndex];
16 | const chosen = currentScene.choices.findIndex((item) => item.choice === choice);
17 | if (chosen < 0) {
18 | atrament.state.setKey(ERROR_STORE_KEY, `[link=${choice}] leads to nonexistent choice!`);
19 | return;
20 | }
21 | makeChoice(chosen);
22 | continueStory();
23 | }, [ atrament, continueStory, makeChoice, choice, atramentState.scenes ]);
24 |
25 | return (
26 |
31 | {children}
32 |
33 | );
34 | }
35 |
36 | export default {
37 | regexp: /\[link=.+?\].+?\[\/link\]/ig,
38 | replacer: (el, markup) => {
39 | const fragments = el.match(/\[link=(.+?)\](.+?)\[\/link\]/i);
40 | return ({markup(fragments[2])});
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/markup/link/index.module.css:
--------------------------------------------------------------------------------
1 | .inline_link, .inline_link:visited {
2 | color: var(--fg-color);
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/markup/picture/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useAtrament } from 'src/atrament/hooks';
3 | import getTagAttributes from 'src/utils/get-tag-attributes';
4 |
5 | import ContainerImage from 'src/components/ui/container-image'
6 |
7 | // [picture align=left]path/to/image.jpg[/picture]
8 |
9 | const Picture = ({ options, src }) => {
10 | const { getAssetPath } = useAtrament();
11 | const pictureOptions = {
12 | fullsize: true
13 | };
14 | ['leftmargin', 'rightmargin', 'width'].forEach((k) => {
15 | if (options[k]) {
16 | pictureOptions[k] = options[k];
17 | }
18 | });
19 | return ();
20 | }
21 |
22 | export default {
23 | regexp: /\[picture(?:\s+[^\]]+)?\].*?\[\/picture\]/ig,
24 | replacer: (el) => {
25 | const fragments = el.match(/\[picture(.*?)\](.*?)\[\/picture\]/i);
26 | const options = getTagAttributes(fragments[1]);
27 | return ();
28 | }
29 | }
--------------------------------------------------------------------------------
/src/components/markup/progress/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | // [progress min=0 max=100 value=99 style=accent]text in bar[/progress]
5 |
6 | import getTagAttributes from 'src/utils/get-tag-attributes';
7 |
8 | const Progress = ({options, children}) => {
9 | const min = +options.min || 0;
10 | const max = +options.max || 100;
11 | const value = +options.value || 0;
12 | let width = value ? ((value - min) * 100)/(max - min) : 0;
13 | if (width > 100) {
14 | width = 100;
15 | }
16 | if (width < 0) {
17 | width = 0;
18 | }
19 |
20 | const classList = [
21 | style.progress_bar,
22 | options.style === 'accent' ? style.accent : style.standard
23 | ].join(' ');
24 |
25 | return (
26 |
27 |
28 |
{children}
29 |
30 | );
31 | };
32 |
33 | export default {
34 | regexp: /\[progress(?:\s+[^\]]+)?\].*?\[\/progress\]/ig,
35 | replacer: (el, markup) => {
36 | const fragments = el.match(/\[progress(.+?)\](.*?)\[\/progress\]/i);
37 | const options = getTagAttributes(fragments[1]);
38 | return ();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/markup/progress/index.module.css:
--------------------------------------------------------------------------------
1 | .progress_frame {
2 | border: 1px solid var(--fg-color);
3 | border-radius: var(--border-radius-inline);
4 | position: relative;
5 | }
6 |
7 | .progress_bar {
8 | position: absolute;
9 | height: 100%;
10 | text-align: left;
11 | opacity: 0.3;
12 | }
13 |
14 | .standard {
15 | background-color: var(--fg-color);
16 | }
17 |
18 | .accent {
19 | background-color: var(--accent-fg-color);
20 | }
21 |
22 | .progress_content {
23 | padding: 0.25rem;
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/markup/spoiler/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useState } from "preact/hooks";
4 |
5 | // [spoiler]text in spoiler[/spoiler]
6 |
7 | const classes = [
8 | style.hidden,
9 | style.revealed
10 | ];
11 |
12 | const Spoiler = ({children}) => {
13 | const [ currentStyle, setStyle ] = useState(0);
14 | return (
15 | setStyle(1 - currentStyle)}
18 | >
19 | {children}
20 |
21 | );
22 | };
23 |
24 | export default {
25 | regexp: /\[spoiler\].*?\[\/spoiler\]/ig,
26 | replacer: (el, markup) => {
27 | const fragments = el.match(/\[spoiler\](.*?)\[\/spoiler\]/i);
28 | return ({markup(fragments[1])});
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/markup/spoiler/index.module.css:
--------------------------------------------------------------------------------
1 | .hidden {
2 | background-color: var(--fg-color);
3 | cursor: pointer;
4 | border-radius: var(--border-radius-inline);
5 | }
6 |
7 | .hidden span {
8 | opacity: 0;
9 | user-select: none;
10 | }
11 |
12 | .revealed {
13 | background-color: var(--bg-color);
14 | transition: background-color var(--animation-disabled, 0.2s) linear;
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/markup/table/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | import getTagAttributes from 'src/utils/get-tag-attributes';
4 |
5 | import Table from 'src/components/ui/table';
6 |
7 | // [table]<>
8 | // [header]Name | Value[/header]<>
9 | // [row][/row]
10 | // [/table]
11 |
12 | const parseHeader = (header, markup) => header
13 | .split(/\[ \]/)
14 | .map((item) => ({
15 | style: {textAlign: 'left'},
16 | name: markup(item)
17 | }));
18 |
19 | const parseRow = (row, markup) => {
20 | const currentRow = row.match(/\[row\](.+?)\[\/row\]/i);
21 | return currentRow[1].split(/\[ \]/).map(markup);
22 | }
23 |
24 | export default {
25 | regexp: /\[table(?:\s+[^\]]+)?\].+?\[\/table\]/ig,
26 | replacer: (el, markup) => {
27 | const fragments = el.match(/\[table(.*?)\](.+?)\[\/table\]/i);
28 | let options = {};
29 | if (fragments[1]) {
30 | options = getTagAttributes(fragments[1]);
31 | }
32 | const tableHeader = fragments[2].match(/\[header\](.+?)\[\/header\]/i);
33 | const tableRows = fragments[2].match(/\[row\].+?\[\/row\]/ig);
34 | const columns = tableHeader ? parseHeader(tableHeader[1], markup) : [];
35 | const rows = tableRows ? tableRows.map((row) => parseRow(row, markup)) : [];
36 | const columnWidth = options.columns ? options.columns.split(/\s+/g) : [];
37 | return ();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/markup/url/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | // [url=URL]Text[/url]
4 |
5 | export default {
6 | regexp: /\[url=.+?\].+?\[\/url\]/ig,
7 | replacer: (el, markup) => {
8 | const fragments = el.match(/\[url=(.+?)\](.+?)\[\/url\]/i);
9 | return ({markup(fragments[2])});
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/markup/video/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useAtrament } from 'src/atrament/hooks';
3 | import getTagAttributes from 'src/utils/get-tag-attributes';
4 |
5 | // [video]path/to/video.mp4[/video]
6 |
7 | const Video = ({ src, options }) => {
8 | const { getAssetPath } = useAtrament();
9 | return (
10 |
20 | );
21 | }
22 |
23 | export default {
24 | regexp: /\[video(?:\s+[^\]]+)?\].+?\[\/video\]/ig,
25 | replacer: (el) => {
26 | const fragments = el.match(/\[video(.*?)\](.+?)\[\/video\]/i);
27 | let options = {};
28 | if (fragments[1]) {
29 | options = getTagAttributes(fragments[1]);
30 | }
31 | return ();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/menu/index.module.css:
--------------------------------------------------------------------------------
1 | .menu_toggle {
2 | position: absolute;
3 | vertical-align: top;
4 | right: 0px;
5 | padding: 0.5rem;
6 | width: 3rem;
7 | height: 3rem;
8 | cursor: pointer;
9 | border: none;
10 | background: none;
11 | color: var(--font-color);
12 | z-index: 150;
13 | }
14 |
15 | .menu_content {
16 | padding: 1rem;
17 | }
18 |
19 | .atrament_version {
20 | margin-top: 1rem;
21 | width: 100%;
22 | text-align: right;
23 | font-size: 0.8rem;
24 | display: flex;
25 | justify-content: flex-end;
26 | }
27 |
28 | .atrament_version:hover {
29 | cursor: pointer;
30 | }
31 |
32 |
33 | .atrament_about {
34 | background-color: var(--fg-color);
35 | color: var(--bg-color);
36 | padding: 0.2em;
37 | }
38 |
39 | .atrament_appversion {
40 | background-color: var(--shade-color);
41 | color: var(--fg-color);
42 | padding: 0.2em;
43 | padding-left: 0.3em;
44 | }
45 |
46 | .atrament_appversion:hover {
47 | background-color: var(--fg-color);
48 | color: var(--bg-color);
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/router.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useCallback } from 'preact/hooks';
3 | import { Router } from 'preact-router';
4 | import { createMemoryHistory } from 'history';
5 |
6 | import { useAtrament } from 'src/atrament/hooks';
7 |
8 | import HomeRoute from 'src/components/routes/home';
9 | import GameRoute from 'src/components/routes/game';
10 | import AboutRoute from 'src/components/routes/about';
11 |
12 | export default function AppRouter() {
13 | const { atrament } = useAtrament();
14 |
15 | const handleRoute = useCallback((route) => {
16 | if (route.url === '/' && route.previous) {
17 | // back from game screen
18 | atrament.game.clear();
19 | }
20 | }, [ atrament.game ]);
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/routes/about.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useEffect, useState } from 'preact/hooks';
3 | import { route } from 'preact-router';
4 | import { Text } from '@eo-locale/preact';
5 |
6 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
7 |
8 | import Menu from 'src/components/menu';
9 |
10 | import Markup from 'src/components/ui/markup';
11 | import Block from 'src/components/ui/block';
12 | import Container from 'src/components/ui/container';
13 | import ContainerText from 'src/components/ui/container-text';
14 | import ContainerFlex from 'src/components/ui/container-flex';
15 | import LinkMenu from 'src/components/ui/link-menu';
16 |
17 | const AboutRoute = () => {
18 | const { evaluateInkFunction } = useAtrament();
19 | const { metadata } = useAtramentState(['metadata']);
20 | const [ aboutContent, setAboutContent ] = useState(' ');
21 | const mainMenu = () => route('/');
22 |
23 | useEffect(() => {
24 | const result = evaluateInkFunction(metadata.about);
25 | if (result.output) {
26 | setAboutContent(result.output);
27 | } else {
28 | setAboutContent(result.error);
29 | }
30 | }, [ metadata.about, setAboutContent, evaluateInkFunction ]);
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default AboutRoute;
--------------------------------------------------------------------------------
/src/components/routes/game.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useEffect } from 'preact/hooks';
3 |
4 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
5 |
6 | import Container from 'src/components/ui/container';
7 | import Menu from 'src/components/menu';
8 |
9 |
10 | import Toolbar from 'src/components/views/toolbar';
11 | import StoryView from 'src/components/views/story';
12 | import OverlayView from 'src/components/views/overlay';
13 | import StoryError from 'src/components/views/story-error';
14 | import { setPageBackground } from 'src/utils/page-background';
15 |
16 | const GameRoute = () => {
17 | const { getAssetPath } = useAtrament();
18 | const atramentState = useAtramentState(['game']);
19 |
20 | let containerStyle;
21 | if (atramentState.game.background) {
22 | containerStyle = {
23 | 'background-image': `url(${getAssetPath(atramentState.game.background)})`,
24 | 'background-size': 'cover',
25 | 'background-position': 'center'
26 | }
27 | }
28 |
29 | const backgroundPage = atramentState.game.background_page;
30 | useEffect(() => {
31 | setPageBackground(backgroundPage, getAssetPath);
32 | }, [ backgroundPage, getAssetPath ]);
33 |
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default GameRoute;
46 |
--------------------------------------------------------------------------------
/src/components/routes/home.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useEffect } from 'preact/hooks';
3 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
4 |
5 | import Container from 'src/components/ui/container';
6 | import ContainerFlex from 'src/components/ui/container-flex';
7 |
8 | import Menu from 'src/components/menu';
9 | import { SessionsMenuView, HomeMenuView } from 'src/components/views/home-menu';
10 |
11 | import { setPageBackground } from 'src/utils/page-background';
12 |
13 | const HomeRoute = () => {
14 | const { canResume, getAssetPath, resetBackground } = useAtrament();
15 | const atramentState = useAtramentState(['metadata']);
16 | const { background, sessions } = atramentState.metadata;
17 |
18 | useEffect(() => {
19 | // reset game background
20 | resetBackground();
21 | // set page background
22 | setPageBackground(background, getAssetPath);
23 | }, [ resetBackground, canResume, background, getAssetPath ]);
24 |
25 | return (
26 |
27 |
28 |
29 | { sessions? : }
30 |
31 |
32 | );
33 | };
34 |
35 | export default HomeRoute;
--------------------------------------------------------------------------------
/src/components/ui/animation-grid/index.jsx:
--------------------------------------------------------------------------------
1 |
2 | import { h } from 'preact';
3 | import style from './style.module.css';
4 |
5 | // https://tobiasahlin.com/spinkit/
6 | // grid effect
7 | const Grid = () => (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
21 | export default Grid;
--------------------------------------------------------------------------------
/src/components/ui/animation-grid/style.module.css:
--------------------------------------------------------------------------------
1 | .sk_cube_grid {
2 | width: 40px;
3 | height: 40px;
4 | }
5 |
6 | .sk_cube_grid div {
7 | width: 33%;
8 | height: 33%;
9 | background-color: var(--fg-color);
10 | float: left;
11 | animation-name: sk-cubeGridScaleDelay;
12 | animation-iteration-count: infinite;
13 | animation-duration: 1.3s;
14 | animation-timing-function: ease-in-out;
15 | }
16 |
17 | .sk_cube1 { animation-delay: 0.2s; }
18 | .sk_cube2 { animation-delay: 0.3s; }
19 | .sk_cube3 { animation-delay: 0.4s; }
20 | .sk_cube4 { animation-delay: 0.1s; }
21 | .sk_cube5 { animation-delay: 0.2s; }
22 | .sk_cube6 { animation-delay: 0.3s; }
23 | .sk_cube7 { animation-delay: 0s; }
24 | .sk_cube8 { animation-delay: 0.1s; }
25 | .sk_cube9 { animation-delay: 0.2s; }
26 |
27 | @-webkit-keyframes sk-cubeGridScaleDelay {
28 | 0%, 70%, 100% {
29 | transform: scale3D(1, 1, 1);
30 | } 35% {
31 | transform: scale3D(0, 0, 1);
32 | }
33 | }
34 |
35 | @keyframes sk-cubeGridScaleDelay {
36 | 0%, 70%, 100% {
37 | transform: scale3D(1, 1, 1);
38 | } 35% {
39 | transform: scale3D(0, 0, 1);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/ui/application-wrapper/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useEffect } from 'preact/hooks';
4 |
5 | let vh;
6 |
7 | function setInnerHeight () {
8 | if (window.innerHeight !== vh) {
9 | vh = window.innerHeight;
10 | document.documentElement.style.setProperty('--screen-vh', `${vh - 1}px`);
11 | }
12 | }
13 |
14 | window.addEventListener('resize', setInnerHeight);
15 |
16 | const ApplicationWrapper = ({ children }) => {
17 | useEffect(setInnerHeight, []);
18 | return (
19 |
20 | {children}
21 |
22 | );
23 | };
24 |
25 | export default ApplicationWrapper;
26 |
--------------------------------------------------------------------------------
/src/components/ui/application-wrapper/index.module.css:
--------------------------------------------------------------------------------
1 | .application_wrapper {
2 | background-color: var(--bg-color);
3 | margin: 0 auto;
4 | height: var(--screen-vh);
5 | }
6 |
7 | @media (min-width: 961px) {
8 | .application_wrapper {
9 | aspect-ratio: 10 / 16;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/ui/backdrop/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const Backdrop = ({ onClick }) => (
5 |
6 | );
7 |
8 | export default Backdrop;
9 |
--------------------------------------------------------------------------------
/src/components/ui/backdrop/index.module.css:
--------------------------------------------------------------------------------
1 | .backdrop {
2 | position: absolute;
3 | width: 100%;
4 | height: 100%;
5 | background: #000000;
6 | opacity: 70%;
7 | z-index: 100;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/ui/block/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const Block = ({ children, align = null }) => {
5 | const cssStyles = [
6 | style.block,
7 | align === 'start' ? style.block_start : '',
8 | align === 'end' ? style.block_end : '',
9 | 'atrament-block'
10 | ].join(' ');
11 | return (
12 |
13 | {children}
14 |
15 | );
16 | };
17 |
18 | export default Block;
19 |
--------------------------------------------------------------------------------
/src/components/ui/block/index.module.css:
--------------------------------------------------------------------------------
1 | .block {
2 | display: block;
3 | margin: 1rem;
4 | }
5 |
6 | .block_start {
7 | margin-bottom: auto;
8 | }
9 |
10 | .block_end {
11 | margin-top: auto;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/ui/break/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const Break = () => (
5 |
6 | );
7 |
8 | export default Break;
9 |
--------------------------------------------------------------------------------
/src/components/ui/break/index.module.css:
--------------------------------------------------------------------------------
1 | .break {
2 | width: 100%;
3 | height: 2em;
4 | min-height: 0.5em;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/ui/close-button/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const CloseButton = ({onClick}) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default CloseButton;
11 |
--------------------------------------------------------------------------------
/src/components/ui/close-button/index.module.css:
--------------------------------------------------------------------------------
1 | .close {
2 | width: 100%;
3 | text-align: right;
4 | }
5 |
6 | .close button {
7 | background-color: var(--bg-color);
8 | color: var(--accent-fg-color);
9 | border: 1px solid var(--accent-fg-color);
10 | font-size: 0.75rem;
11 | padding: 0.5rem;
12 | text-align: center;
13 | text-decoration: none;
14 | border-radius: var(--border-radius);
15 | cursor: pointer;
16 | }
17 |
18 | .close button:hover {
19 | background-color: var(--accent-fg-color);
20 | color: var(--bg-color);
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/ui/collapse/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const Collapse = ({ title, children, open = false }) => (
5 |
6 | {title}
7 |
8 | {children}
9 |
10 |
11 | );
12 |
13 | export default Collapse;
14 |
--------------------------------------------------------------------------------
/src/components/ui/collapse/index.module.css:
--------------------------------------------------------------------------------
1 | .collapse {
2 | border-radius: var(--border-radius-inline);
3 | border: 3px solid var(--shade-color);
4 | }
5 |
6 | .collapse_title {
7 | cursor: pointer;
8 | background-color: var(--shade-color);
9 | padding: 0.5em;
10 | }
11 |
12 | .collapse_children {
13 | padding: 0.5em;
14 | display: flex;
15 | flex-direction: column;
16 | gap: 1em;
17 | overflow: auto;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/ui/container-flex/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const ContainerFlex = ({ children }) => (
5 |
6 | {children}
7 |
8 | );
9 |
10 | export default ContainerFlex;
11 |
--------------------------------------------------------------------------------
/src/components/ui/container-flex/index.module.css:
--------------------------------------------------------------------------------
1 | .container_flex {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: flex-start;
5 | overflow: auto;
6 | height: 100%;
7 | }
8 |
9 | /*
10 | .container_flex::-webkit-scrollbar {
11 | display: none;
12 | }
13 | */
14 |
--------------------------------------------------------------------------------
/src/components/ui/container-image/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const ContainerImage = ({ src, options = {} }) => (
5 |
12 |

23 |
24 | );
25 |
26 | export default ContainerImage;
--------------------------------------------------------------------------------
/src/components/ui/container-image/index.module.css:
--------------------------------------------------------------------------------
1 | .imagebox {
2 | display: flex;
3 | }
4 |
5 | .image {
6 | margin: auto;
7 | max-width: 100%;
8 | }
9 |
10 | .illustration {
11 | max-height: 50vh;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/ui/container-modal/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const ContainerModal = ({ children }) => (
5 |
6 | {children}
7 |
8 | );
9 |
10 | export default ContainerModal;
--------------------------------------------------------------------------------
/src/components/ui/container-modal/index.module.css:
--------------------------------------------------------------------------------
1 | .container_modal {
2 | position: absolute;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: flex-start;
6 | overflow: auto;
7 | width: 100%;
8 | height: 100%;
9 | z-index: 150;
10 | }
--------------------------------------------------------------------------------
/src/components/ui/container-text/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const ContainerText = ({ children }) => {
5 | return (
6 |
7 | {children}
8 |
9 | );
10 | };
11 |
12 | export default ContainerText;
13 |
--------------------------------------------------------------------------------
/src/components/ui/container-text/index.module.css:
--------------------------------------------------------------------------------
1 | .container_text {
2 | flex-grow: 1;
3 | flex-shrink: 1;
4 | flex-basis: auto;
5 | display: flex;
6 | flex-direction: column;
7 | overflow: auto;
8 | line-height: calc(1ex / 0.32);
9 | font-family: var(--font-game);
10 | font-size: var(--game-font-size);
11 | }
12 |
13 | /*
14 | .container_text::-webkit-scrollbar {
15 | display: none;
16 | }
17 | */
18 |
--------------------------------------------------------------------------------
/src/components/ui/container/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import componentStyle from './index.module.css';
3 |
4 | const Container = ({ children, style = {} }) => (
5 |
6 | {children}
7 |
8 | );
9 |
10 | export default Container;
11 |
--------------------------------------------------------------------------------
/src/components/ui/container/index.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | background-color: var(--bg-color);
3 | position: relative;
4 | height: 100%;
5 | display: flex;
6 | flex-flow: column;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/ui/error-modal/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | import ContainerModal from '../container-modal';
5 | import Backdrop from 'src/components/ui/backdrop';
6 | import Modal from 'src/components/ui/modal';
7 | import CloseButton from 'src/components/ui/close-button';
8 | import Block from 'src/components/ui/block';
9 |
10 | const noop = () => {};
11 |
12 | const ErrorModal = ({ close = noop, message }) => {
13 | return (
14 |
15 |
16 |
17 |
18 | {close !== noop ?
: ''}
19 |
20 | {message}
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default ErrorModal;
29 |
--------------------------------------------------------------------------------
/src/components/ui/error-modal/index.module.css:
--------------------------------------------------------------------------------
1 | .error_modal_content {
2 | color: var(--accent-bg-color);
3 | background-color: var(--accent-fg-color);
4 | padding: 1rem;
5 | font-size: 120%;
6 | }
7 |
8 | .error_message {
9 | font-family: monospace;
10 | margin-top: 0;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/ui/header/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const Header = ({ children }) => (
5 |
10 | );
11 |
12 | export default Header;
--------------------------------------------------------------------------------
/src/components/ui/header/index.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | flex-grow: 1;
3 | text-align: center;
4 | display: flex;
5 | margin: 1rem;
6 | font-family: var(--font-game);
7 | }
8 |
9 | .header_align {
10 | margin: auto;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/ui/icons/index.jsx:
--------------------------------------------------------------------------------
1 | export const IconMenu = () => (
2 |
3 | );
4 |
5 | export const IconVolumeHigh = () => (
6 |
7 | );
8 |
9 | export const IconVolumeLow = () => (
10 |
11 | );
12 |
13 | export const IconToolbarBack = () => (
14 |
15 | );
16 |
17 | export const IconDebugger = () => (
18 |
19 | );
20 |
--------------------------------------------------------------------------------
/src/components/ui/link-home/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const LinkHome = ({ children, onClick }) => {
5 | return (
6 |
7 | );
8 | };
9 |
10 | export default LinkHome;
11 |
--------------------------------------------------------------------------------
/src/components/ui/link-home/index.module.css:
--------------------------------------------------------------------------------
1 | .link_home {
2 | display: block;
3 | background-color: var(--bg-color);
4 | color: var(--accent-fg-color);
5 | padding: 1rem;
6 | text-align: center;
7 | text-decoration: none;
8 | width: 100%;
9 | border: 1px solid var(--accent-fg-color);
10 | border-radius: var(--border-radius);
11 | cursor: pointer;
12 | }
13 |
14 | .link_home:hover {
15 | background-color: var(--accent-fg-color);
16 | color: var(--accent-bg-color);
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/ui/link-menu/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const LinkMenu = ({children, key, onClick, attributes={}}) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default LinkMenu;
--------------------------------------------------------------------------------
/src/components/ui/link-menu/index.module.css:
--------------------------------------------------------------------------------
1 | .link_menu {
2 | display: block;
3 | width: 100%;
4 | background-color: var(--bg-color);
5 | color: var(--fg-color);
6 | margin-top: 1rem;
7 | padding: 1rem;
8 | text-align: center;
9 | text-decoration: none;
10 | border: 1px solid var(--fg-color);
11 | border-radius: var(--border-radius);
12 | cursor: pointer;
13 | font-family: var(--font-game);
14 | }
15 |
16 | .link_menu:hover {
17 | background-color: var(--fg-color);
18 | color: var(--bg-color);
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/ui/loading/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | import Container from '../container';
5 | import ContainerFlex from '../container-flex';
6 |
7 | import Spinner from '../animation-grid';
8 |
9 | const Loading = () => (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
19 | export default Loading;
--------------------------------------------------------------------------------
/src/components/ui/loading/index.module.css:
--------------------------------------------------------------------------------
1 | .loading {
2 | margin: auto;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/ui/markup/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useMemo } from 'preact/hooks';
3 | import markup from 'src/atrament/markup';
4 | import { ActiveContentContext } from 'src/context';
5 |
6 | const Markup = ({ content, isActive=true }) => {
7 | const transformedContent = useMemo(() => markup(content), [ content ]);
8 | return(
9 |
10 | {transformedContent}
11 |
12 | );
13 | };
14 |
15 | export default Markup;
16 |
--------------------------------------------------------------------------------
/src/components/ui/menu-list-item/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useState } from 'preact/hooks';
3 | import { Text } from '@eo-locale/preact';
4 | import style from './index.module.css';
5 |
6 | const DialogYesNo = ({ prompt, onAccept, onReject, attributes}) => (
7 |
8 |
{prompt}
9 |
12 |
15 |
16 | );
17 |
18 |
19 | const MenuListItem = ({
20 | children,
21 | key,
22 | onSelect,
23 | isDisabled = false,
24 | isDeletable = false,
25 | onDelete = ()=>{},
26 | deletePrompt = '',
27 | hasConfirmation = false,
28 | confirmPrompt = '',
29 | attributes={}
30 | }) => {
31 | const [ isDeleteDialog, displayDeleteDialog ] = useState(false);
32 | const [ isConfirmDialog, displayConfirmDialog ] = useState(false);
33 |
34 | const showDeleteDialog = () => displayDeleteDialog(true);
35 | const hideDeleteDialog = () => displayDeleteDialog(false);
36 | const handleDelete = (ev) => {
37 | onDelete(ev);
38 | hideDeleteDialog();
39 | }
40 |
41 | const showConfirmDialog = () => displayConfirmDialog(true);
42 | const hideConfirmDialog = () => displayConfirmDialog(false);
43 | const handleConfirm = (ev) => {
44 | onSelect(ev);
45 | hideConfirmDialog();
46 | }
47 |
48 | if (isConfirmDialog) {
49 | return ();
50 | } else if (isDeleteDialog) {
51 | return ();
52 | }
53 |
54 | return (
55 |
56 |
65 | {(isDeletable && !isDisabled) &&
66 | }
74 |
75 | );
76 | };
77 |
78 | export default MenuListItem;
79 |
--------------------------------------------------------------------------------
/src/components/ui/menu-list-item/index.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | position: relative;
4 | gap: 0.5em;
5 | }
6 |
7 | .prompt {
8 | flex-grow: 2;
9 | width: 100%;
10 | margin-top: 1rem;
11 | padding: 1rem;
12 | padding-left: 0.5rem;
13 | }
14 |
15 | .menu_item {
16 | flex-grow: 1;
17 | display: block;
18 | background-color: var(--bg-color);
19 | color: var(--fg-color);
20 | margin-top: 1rem;
21 | padding: 1rem;
22 | padding-left: 2.5rem;
23 | padding-right: 2.5rem;
24 | text-align: center;
25 | text-decoration: none;
26 | border: 1px solid var(--fg-color);
27 | border-radius: var(--border-radius);
28 | cursor: pointer;
29 | font-family: var(--font-game);
30 | }
31 |
32 | .menu_item:hover {
33 | background-color: var(--fg-color);
34 | color: var(--bg-color);
35 | }
36 |
37 | .menu_item:disabled {
38 | background-color: var(--shade-color);
39 | color: var(--fg-color);
40 | opacity: 0.6;
41 | cursor: inherit;
42 | }
43 |
44 | .menu_item:disabled:hover {
45 | background-color: var(--shade-color);
46 | color: var(--fg-color);
47 | }
48 |
49 | .small {
50 | padding-left: 1rem;
51 | padding-right: 1rem;
52 | }
53 |
54 | .is_deletable {
55 | border-right: 0px;
56 | }
57 |
58 | .delete_button {
59 | position: absolute;
60 | width: 2em;
61 | height: calc(100% - 1rem);
62 | padding-left: 0;
63 | padding-right: 0;
64 | right: 0px;
65 | top: 0px;
66 | border-top-left-radius: 0;
67 | border-bottom-left-radius: 0;
68 | text-align: center;
69 | background-color: var(--accent-fg-color);
70 | color: var(--accent-bg-color);
71 | border: 1px solid var(--accent-fg-color);
72 | }
73 |
74 | .delete_button:hover {
75 | background-color: var(--accent-fg-color);
76 | color: var(--accent-bg-color);
77 | }
78 |
79 | .delete_item {
80 | flex-grow: 1;
81 | text-align: center;
82 | background-color: var(--accent-bg-color);
83 | color: var(--accent-fg-color);
84 | border: 1px solid var(--accent-fg-color);
85 | }
86 |
87 | .delete_item:hover {
88 | background-color: var(--accent-fg-color);
89 | color: var(--accent-bg-color);
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/ui/modal/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const Modal = ({ children }) => (
5 |
6 | {children}
7 |
8 | );
9 |
10 | export default Modal;
11 |
--------------------------------------------------------------------------------
/src/components/ui/modal/index.module.css:
--------------------------------------------------------------------------------
1 | .modal {
2 | margin: auto;
3 | width: 80%;
4 | background: var(--bg-color);
5 | opacity: 100%;
6 | z-index: 200;
7 | display: flex;
8 | flex-direction: column;
9 | overflow: auto;
10 | border-radius: var(--border-radius);
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/ui/table/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useState, useEffect } from 'preact/hooks';
3 | import style from './index.module.css';
4 |
5 | const Table = ({columns = [], data = [], pageSize = 0, border = true, padding = true, columnWidth = []}) => {
6 | const [ currentPage, setCurrentPage ] = useState(0);
7 | useEffect(() => {
8 | setCurrentPage(0);
9 | }, [data]);
10 |
11 | let pages = 1;
12 | let startFrom = 0;
13 | let endOn = data.length;
14 | if (pageSize) {
15 | pages = Math.ceil(data.length/pageSize);
16 | startFrom = currentPage * pageSize;
17 | endOn = startFrom + pageSize;
18 | }
19 |
20 | const goToPage = (e) => {
21 | setCurrentPage(+e.target.value);
22 | }
23 | const displayData = data.slice(startFrom, endOn);
24 |
25 | return (
26 |
27 | {pageSize > 0 &&
28 |
29 | {[...Array(pages).keys()].map((p) =>
30 |
38 | )}
39 |
40 | }
41 | {columns.length > 0 &&
42 |
43 |
44 | {columns.map((th, i) => {
45 | const thStyle = th.style || {};
46 | if (columnWidth[i]) {
47 | thStyle.width = columnWidth[i];
48 | }
49 | return (
50 | {th.name} |
51 | )
52 | })}
53 |
54 |
55 | }
56 | {displayData.length > 0 &&
57 |
58 | {displayData.map((tr, row) => (
59 |
60 | {tr.map((td, col) => {
61 | const tdStyle = {};
62 | if (columnWidth[col]) {
63 | tdStyle.width = columnWidth[col];
64 | }
65 | return (
66 | {td} |
67 | );
68 | })}
69 |
70 | ))}
71 |
72 | }
73 |
74 | );
75 | }
76 |
77 | export default Table;
78 |
--------------------------------------------------------------------------------
/src/components/ui/table/index.module.css:
--------------------------------------------------------------------------------
1 | .table {
2 | border-collapse: collapse;
3 | color: var(--fg-color);
4 | width: 100%;
5 | }
6 |
7 | .cell_full {
8 | padding: 0;
9 | }
10 |
11 | .cell_padding {
12 | padding: 0.5em 0.5em;
13 | }
14 |
15 | .thead_border {
16 | border-bottom: 2px solid var(--shade-color);
17 | }
18 |
19 | .tbody_border {
20 | border-bottom: 1px solid var(--shade-color);
21 | }
22 |
23 | .table tbody tr:last-of-type {
24 | border-bottom: none;
25 | }
26 |
27 | .caption {
28 | caption-side: bottom;
29 | }
30 |
31 | .caption button {
32 | cursor: pointer;
33 | border: none;
34 | border-radius: var(--border-radius-inline);
35 | background-color: var(--shade-color);
36 | margin: 0.25rem;
37 | padding: 0.25rem;
38 | }
39 |
40 | .page {
41 | color: var(--fg-color);
42 | }
43 |
44 | .page_active {
45 | color: var(--accent-fg-color);
46 | }
--------------------------------------------------------------------------------
/src/components/ui/tabs/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useState } from 'preact/hooks';
4 |
5 | const Tab = ({ children}) => {children}
;
6 |
7 | const TabContent = ({ children, isHidden }) => (
8 |
11 | );
12 |
13 | const TabHeader = ({ children, title, isActive, onSelect }) => (
14 |
17 | );
18 |
19 | const Tabs = ({ children }) => {
20 | const [ activeTab, setActiveTab ] = useState(0);
21 |
22 | return (
23 |
24 |
25 | {children.map((tab, id) =>
26 | setActiveTab(id)}
31 | >
32 | {tab.props.title}
33 |
34 | )}
35 |
36 | {children.map((tab, id) =>
37 |
{tab.props.children}
38 | )}
39 |
40 | );
41 | }
42 |
43 | export {
44 | Tab,
45 | Tabs
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/ui/tabs/index.module.css:
--------------------------------------------------------------------------------
1 | .tab {
2 |
3 | }
4 |
5 | .tablist {
6 | display: flex;
7 | flex-direction: row;
8 | margin: 0;
9 | padding: 0;
10 | border-bottom: 1px solid var(--fg-color);
11 | gap: 1em;
12 | }
13 |
14 | .tabheader {
15 | display: flex;
16 | color: var(--fg-color);
17 | }
18 |
19 | .tabheader a {
20 | display: block;
21 | text-decoration: none;
22 | padding: 0.5em;
23 | color: var(--fg-color);
24 | }
25 |
26 | .tabheader a.active {
27 | color: var(--accent-fg-color);
28 | }
29 |
30 | .tabheader a:hover {
31 | color: var(--bg-color);
32 | background-color: var(--fg-color);
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/src/components/ui/text-paragraph/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const TextParagraph = ({ children }) => (
5 |
6 | {children}
7 |
8 | );
9 |
10 | export default TextParagraph;
11 |
--------------------------------------------------------------------------------
/src/components/ui/text-paragraph/index.module.css:
--------------------------------------------------------------------------------
1 | .text_paragraph {
2 | display: block;
3 | margin-block-end: 1em;
4 | margin-inline-start: 0;
5 | margin-inline-end: 0;
6 | unicode-bidi: isolate;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/ui/toggle/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const Toggle = ({enabled, onChange}) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default Toggle;
11 |
12 |
--------------------------------------------------------------------------------
/src/components/ui/toggle/index.module.css:
--------------------------------------------------------------------------------
1 | .checkbox_wrapper {
2 | display: inline-block;
3 | padding: 1px;
4 | }
5 |
6 | .checkbox_wrapper .checkbox {
7 | appearance: none;
8 | background-color: var(--bg-color);
9 | border-radius: 1rem;
10 | border: 1px solid var(--fg-color);
11 | flex-shrink: 0;
12 | height: 1.1rem;
13 | margin: 0;
14 | position: relative;
15 | width: 2rem;
16 | }
17 |
18 | .checkbox_wrapper .checkbox::before {
19 | bottom: -6px;
20 | left: -6px;
21 | right: -6px;
22 | top: -6px;
23 | content: "";
24 | position: absolute;
25 | }
26 |
27 | .checkbox_wrapper .checkbox,
28 | .checkbox_wrapper .checkbox::after {
29 | transition: all 100ms ease-out;
30 | }
31 |
32 | .checkbox_wrapper .checkbox::after {
33 | background-color: var(--fg-color);
34 | border-radius: 50%;
35 | content: "";
36 | height: 0.9rem;
37 | left: 1px;
38 | position: absolute;
39 | top: 1px;
40 | width: 0.9rem;
41 | }
42 |
43 | .checkbox_wrapper input[type=checkbox] {
44 | cursor: default;
45 | }
46 |
47 | .checkbox_wrapper .checkbox:checked {
48 | background-color: var(--fg-color);
49 | }
50 |
51 | .checkbox_wrapper .checkbox:checked::after {
52 | background-color: var(--bg-color);
53 | left: 0.95rem;
54 | }
55 |
56 | .checkbox_wrapper :focus:not(.focus-visible) {
57 | outline: 0;
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/views/about/atrament-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/src/components/views/about/atrament-logo.png
--------------------------------------------------------------------------------
/src/components/views/about/iftf-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/technix/atrament-web-ui/6086816b0074b3c022201d801b9b20feafbf45f8/src/components/views/about/iftf-logo.png
--------------------------------------------------------------------------------
/src/components/views/about/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Text } from '@eo-locale/preact';
3 |
4 | import { appVersion } from 'src/constants';
5 | import Block from 'src/components/ui/block';
6 |
7 | import style from './index.module.css';
8 | import atramentLogoImage from './atrament-logo.png';
9 | import iftfLogoImage from './iftf-logo.png';
10 |
11 | const components = [
12 | ["Atrament Core", "https://github.com/technix/atrament-core"],
13 | ["Atrament Web", "https://github.com/technix/atrament-web"],
14 | ["Atrament Web UI", "https://github.com/technix/atrament-web-ui"],
15 | ["InkJS", "https://github.com/y-lohse/inkjs"],
16 | ["Preact", "https://preactjs.com/"],
17 | ["Howler", "https://howlerjs.com/"],
18 | ["Nanostores", "https://github.com/nanostores/nanostores"],
19 | ["LocalForage", "https://github.com/localForage/localForage"],
20 | ["fflate", "https://github.com/101arrowz/fflate"],
21 | ];
22 | const lastComponent = components.length - 1;
23 |
24 | const A = ({href, children}) => ({children});
25 | const iftfLink = 'Interactive Fiction Technology Foundation';
26 |
27 | const AboutMenu = ({ onClick }) => {
28 | const clickHandler = (e) => e.target.tagName.toLowerCase() === 'a' ? '' : onClick(e);
29 | return (
30 |
31 |
32 |
36 |
https://atrament.ink/
37 |
38 | : {components.map((item, index) => (
39 |
40 | {item[0]}
41 | {index === lastComponent ? '.' : ','}
42 | ))}
43 |
44 |
45 |

46 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default AboutMenu;
--------------------------------------------------------------------------------
/src/components/views/about/index.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | align-items: center;
4 | margin: 0px;
5 | }
6 |
7 | .header img {
8 | display: block;
9 | flex-grow: 0;
10 | height: 5em;
11 | margin-right: 1em;
12 | }
13 |
14 | .header small {
15 | display: block;
16 | opacity: 0.8;
17 | }
18 |
19 | .infobox {
20 | display: flex;
21 | align-items: center;
22 | }
23 |
24 | .infobox img {
25 | width: 20%;
26 | flex-grow: 0;
27 | margin-right: 1em;
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/views/debugger/functions.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useState } from 'preact/hooks';
4 | import { useAtrament } from 'src/atrament/hooks';
5 | import { Text, useTranslator } from '@eo-locale/preact';
6 |
7 | import Collapse from 'src/components/ui/collapse';
8 | import Table from 'src/components/ui/table';
9 |
10 | const DebugFunctions = () => {
11 | const { atrament } = useAtrament();
12 | const translator = useTranslator();
13 | const [ fnName, setFnName ] = useState('');
14 | const [ fnArgs, setFnArgs ] = useState(null);
15 | const [ errorMsg, setErrorMsg ] = useState('');
16 | const [ outputMsg, setOutputMsg ] = useState(null);
17 |
18 | const parseArgs = (args) => {
19 | let fnArgs = [];
20 | try {
21 | fnArgs = JSON.parse(args);
22 | } catch (e) {
23 | // ignore parsing error
24 | }
25 | return fnArgs;
26 | }
27 |
28 | const handleFnChange = (e) => {
29 | setFnName(e.target.value);
30 | setErrorMsg('');
31 | setOutputMsg(null);
32 | };
33 |
34 | const handleArgsChange = (e) => {
35 | setFnArgs(e.target.value);
36 | setErrorMsg('');
37 | setOutputMsg(null);
38 | };
39 |
40 | const runFunction = () => {
41 | if (fnName) {
42 | try {
43 | const result = atrament.ink.evaluateFunction(fnName, parseArgs(fnArgs), true);
44 | setOutputMsg(result);
45 | setErrorMsg('');
46 | } catch (e) {
47 | setErrorMsg(e.toString());
48 | }
49 | }
50 | };
51 |
52 | return(
53 |
54 |
55 |
56 | :
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | :
65 |
66 |
67 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
{fnName && `${fnName}(${fnArgs ? parseArgs(fnArgs).map(JSON.stringify).join(', ') : ''})`}
82 |
83 | {errorMsg &&
84 | {errorMsg}
85 |
}
86 |
87 | {outputMsg &&
{outputMsg.output}],
90 | ]} />}
91 |
92 |
93 | );
94 | };
95 |
96 | export default DebugFunctions;
97 |
--------------------------------------------------------------------------------
/src/components/views/debugger/globaltags.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | import { KNOWN_GLOBAL_TAGS } from 'src/constants';
5 |
6 | import { useAtrament } from 'src/atrament/hooks';
7 | import { useTranslator } from '@eo-locale/preact';
8 |
9 | import Collapse from 'src/components/ui/collapse';
10 | import Table from 'src/components/ui/table';
11 |
12 | const DebugGlobaltags = () => {
13 | const { atrament } = useAtrament();
14 | const translator = useTranslator();
15 | const globaltags = atrament.ink.getGlobalTags();
16 |
17 | const displayGlobaltags = Object.keys(globaltags).map(k => {
18 | if (KNOWN_GLOBAL_TAGS.includes(k)) {
19 | return [k, globaltags[k]];
20 | }
21 | return [(
22 | {k}), globaltags[k]];
27 | });
28 |
29 | return(
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default DebugGlobaltags;
37 |
--------------------------------------------------------------------------------
/src/components/views/debugger/goto.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { Text } from '@eo-locale/preact';
4 | import { useState } from 'preact/hooks';
5 | import { useAtrament } from 'src/atrament/hooks';
6 |
7 | const DebugGoto = ({closeFn}) => {
8 | const { atrament } = useAtrament();
9 | const [ pathString, setPathString ] = useState('');
10 | const [ errorMsg, setErrorMsg ] = useState('');
11 |
12 | const handlePathstringChange = (e) => {
13 | setPathString(e.target.value);
14 | setErrorMsg('');
15 | };
16 | const goToPath = () => {
17 | if (pathString) {
18 | try {
19 | atrament.ink.goTo(pathString);
20 | atrament.game.continueStory();
21 | closeFn();
22 | } catch (e) {
23 | setErrorMsg(e.toString());
24 | }
25 | setPathString('');
26 | }
27 | };
28 |
29 | return(
30 | <>
31 |
32 |
33 | :
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {errorMsg}
44 |
45 | >
46 | );
47 | };
48 |
49 | export default DebugGoto;
50 |
--------------------------------------------------------------------------------
/src/components/views/debugger/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useEffect, useCallback, useState } from 'preact/hooks';
3 | import { useTranslator } from '@eo-locale/preact';
4 | import style from './index.module.css';
5 |
6 | import { useAtramentState } from 'src/atrament/hooks';
7 | import CloseButton from 'src/components/ui/close-button';
8 | import { IconDebugger } from 'src/components/ui/icons';
9 |
10 | import DebugInfo from './info';
11 | import DebugGoto from './goto';
12 | import DebugGlobaltags from './globaltags';
13 | import DebugVariables from './variables';
14 | import DebugVisits from './visits';
15 | import DebugFunctions from './functions';
16 |
17 | const DebuggerView = () => {
18 | const [ isOpen, openDebugger ] = useState(false);
19 | const [ keyWait, setKeyWait ] = useState(false);
20 | const translator = useTranslator();
21 |
22 | const toggleDebugger = useCallback(() => openDebugger(!isOpen), [ isOpen ]);
23 |
24 | const debugHandler = useCallback((e) => {
25 | if (e.code === 'Backquote') {
26 | if (keyWait) {
27 | toggleDebugger();
28 | setKeyWait(false);
29 | } else {
30 | setKeyWait(true);
31 | setTimeout(() => setKeyWait(false), 1000);
32 | }
33 | }
34 | }, [ toggleDebugger, keyWait, setKeyWait ]);
35 |
36 | useEffect(() => {
37 | document.addEventListener("keydown", debugHandler, false);
38 | return () => {
39 | document.removeEventListener("keydown", debugHandler, false);
40 | }
41 | }, [ debugHandler ]);
42 |
43 | if (!isOpen) {
44 | return ();
45 | }
46 |
47 | return (
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | }
59 |
60 | const DebuggerMenu = () => {
61 | const { metadata } = useAtramentState(['metadata']);
62 | return metadata.debug ? : <>>;
63 | }
64 |
65 | export default DebuggerMenu;
66 |
--------------------------------------------------------------------------------
/src/components/views/debugger/index.module.css:
--------------------------------------------------------------------------------
1 | .debug_toggle {
2 | position: absolute;
3 | top: 3em;
4 | right: 0.6em;
5 | padding: 0.5rem;
6 | width: 2em;
7 | cursor: pointer;
8 | border: none;
9 | border-radius: var(--border-radius);
10 | background-color: var(--shade-color);
11 | color: var(--accent-fg-color);
12 | z-index: 300;
13 | }
14 |
15 | .debug_container {
16 | position: absolute;
17 | display: flex;
18 | flex-direction: column;
19 | justify-content: flex-start;
20 | width: 100%;
21 | height: 100%;
22 | background: var(--bg-color);
23 | border: 5px solid var(--accent-fg-color);
24 | padding: 0.5rem;
25 | z-index: 1500;
26 | gap: 1em;
27 | overflow: auto;
28 | }
29 |
30 | .container {
31 | display: flex;
32 | flex-direction: row;
33 | align-items: center;
34 | width: 100%;
35 | }
36 |
37 | .container div {
38 | vertical-align: middle;
39 | padding: 0.2rem;
40 | }
41 |
42 | .container pre {
43 | margin-bottom: 0;
44 | }
45 |
46 | .input_div {
47 | flex-grow: 1;
48 | }
49 |
50 | .button {
51 | background-color: var(--bg-color);
52 | color: var(--fg-color);
53 | text-decoration: none;
54 | cursor: pointer;
55 | padding: 0.25rem;
56 | border: 2px solid var(--fg-color);
57 | border-radius: var(--border-radius-inline);
58 | }
59 |
60 | .button:hover {
61 | background-color: var(--fg-color);
62 | color: var(--bg-color);
63 | }
64 |
65 | .input {
66 | width: 100%;
67 | padding: 0.25rem;
68 | border: 2px solid var(--fg-color);
69 | border-radius: var(--border-radius-inline);
70 | background-color: var(--bg-color);
71 | color: var(--fg-color);
72 | }
73 |
74 | .errormsg {
75 | color: var(--accent-fg-color);
76 | }
77 |
78 | .outputmsg {
79 | font-family: monospace;
80 | }
81 |
82 | .unknown_tag {
83 | color: var(--accent-fg-color);
84 | font-weight: bold;
85 | cursor: help;
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/views/debugger/info.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useAtrament } from 'src/atrament/hooks';
3 | import { useTranslator } from '@eo-locale/preact';
4 |
5 | import Collapse from 'src/components/ui/collapse';
6 | import Table from 'src/components/ui/table';
7 |
8 | const DebugInfo = () => {
9 | const { atrament } = useAtrament();
10 | const translator = useTranslator();
11 |
12 | const inkstory = atrament.ink.story();
13 | const inkstate = inkstory.state;
14 | const gamedata = atrament.state.get().game;
15 |
16 | const tableData = [
17 | [translator.translate('debug.info.ink-file'), `${gamedata.$path}/${gamedata.$file}`],
18 | [translator.translate('debug.info.story-seed'), inkstate.storySeed],
19 | [translator.translate('debug.info.current-turn-index'), inkstate.currentTurnIndex],
20 | [translator.translate('debug.info.path'), gamedata.$story_path],
21 | ];
22 |
23 | return(
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default DebugInfo;
--------------------------------------------------------------------------------
/src/components/views/debugger/var-editor.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './var-editor.module.css';
3 | import { useEffect, useState } from 'preact/hooks';
4 | import { useTranslator } from '@eo-locale/preact';
5 | import { useAtrament } from 'src/atrament/hooks';
6 |
7 | import Toggle from 'src/components/ui/toggle';
8 |
9 | const NumberEditor = ({value, setNewValue}) => {
10 | const updateValue = (e) => setNewValue(+e.target.value);
11 | return ();
12 | };
13 |
14 | const StringEditor = ({value, setNewValue}) => {
15 | const updateValue = (e) => setNewValue(e.target.value);
16 | return ();
17 | };
18 |
19 | const BooleanEditor = ({value, setNewValue}) => {
20 | const updateValue = (e) => setNewValue(e.target.checked);
21 | return (
);
22 | };
23 |
24 | const ListViewer = ({name, value}) => {
25 | if (name === value._originNames[0] && value.size === 0) {
26 | // empty list, show only possible keys
27 | const mapKeys = [...value.origins[0]._itemNameToValues.keys()];
28 | return (
29 | {JSON.stringify(mapKeys).replace(/,/g, ', ')}
30 | );
31 | }
32 | const arr = [];
33 | value.forEach((v, k) => arr.push([JSON.parse(k), v]));
34 | return (
35 |
36 | {arr.map(([k, v]) => - {k.itemName}: {v}
)}
37 |
38 | );
39 | }
40 |
41 |
42 |
43 | const DebugVariableEditor = ({name, value}) => {
44 | const { getInkVariable, setInkVariable } = useAtrament();
45 | const translator = useTranslator();
46 | const [ editMode, setEditMode ] = useState(false);
47 | const [ initialValue, setInitialValue ] = useState(false);
48 | const [ newValue, setNewValue ] = useState(false);
49 |
50 | useEffect(() => {
51 | const v = getInkVariable(name);
52 | setInitialValue(v);
53 | setNewValue(v);
54 | }, [ name, getInkVariable, setInitialValue, setNewValue ]);
55 |
56 | const handleEdit = (e) => {
57 | e.preventDefault();
58 | setEditMode(true);
59 | };
60 |
61 | const cancelEdit = () => setEditMode(false);
62 |
63 | const saveValue = () => {
64 | if (newValue !== initialValue) {
65 | setInkVariable(name, newValue);
66 | setInitialValue(newValue);
67 | }
68 | setEditMode(false);
69 | }
70 |
71 | return (
72 | editMode ?
73 | <>
74 |
75 | {typeof value === 'number' && }
76 | {typeof value === 'string' && }
77 | {typeof value === 'boolean' && }
78 |
79 | >
80 | :
81 | <>
82 | {typeof newValue === 'object'
83 | ?
84 | :
85 | }
86 | >
87 | );
88 | }
89 |
90 | export default DebugVariableEditor;
91 |
--------------------------------------------------------------------------------
/src/components/views/debugger/var-editor.module.css:
--------------------------------------------------------------------------------
1 | .button {
2 | background-color: var(--bg-color);
3 | color: var(--fg-color);
4 | text-decoration: none;
5 | font-weight: bold;
6 | cursor: pointer;
7 | padding: 0.25rem;
8 | border: 1px solid var(--fg-color);
9 | border-radius: var(--border-radius-inline);
10 | }
11 |
12 | .button:hover {
13 | background-color: var(--fg-color);
14 | color: var(--bg-color);
15 | }
16 |
17 | .button_close {
18 | background-color: var(--bg-color);
19 | color: var(--accent-fg-color);
20 | text-decoration: none;
21 | font-weight: bold;
22 | cursor: pointer;
23 | padding: 0.25rem;
24 | border: 1px solid var(--fg-color);
25 | border-radius: var(--border-radius-inline);
26 | }
27 |
28 | .button_close:hover {
29 | background-color: var(--accent-fg-color);
30 | color: var(--accent-bg-color);
31 | }
32 |
33 | .input {
34 | width: 10rem;
35 | padding: 0.25rem;
36 | border: 1px solid var(--fg-color);
37 | border-radius: var(--border-radius-inline);
38 | background-color: var(--bg-color);
39 | color: var(--fg-color);
40 | }
41 |
42 | .checkbox {
43 | width: 2rem;
44 | }
45 |
46 | .toggle {
47 | display: inline-block;
48 | margin-left: 1rem;
49 | margin-right: 1rem;
50 | }
51 |
52 | .listview {
53 | font-family: monospace;
54 | }
--------------------------------------------------------------------------------
/src/components/views/debugger/variables.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useState } from 'preact/hooks';
4 | import { useAtrament } from 'src/atrament/hooks';
5 | import { useTranslator } from '@eo-locale/preact';
6 |
7 | import Collapse from 'src/components/ui/collapse';
8 | import Table from 'src/components/ui/table';
9 |
10 | import DebugVariableEditor from './var-editor';
11 |
12 | function listInkVariables(atrament) {
13 | const varState = atrament.ink.getVariables();
14 | const inkVariables = Object.entries(varState);
15 | return inkVariables.sort((a, b) => a[0] > b[0] ? 0 : -1);
16 | }
17 |
18 | const DebugVariablesTable = ({ variables }) => {
19 | const translator = useTranslator();
20 | const tableData = variables.map(
21 | (item, i) => [ item[0], ]
22 | );
23 | return(
24 |
30 | );
31 | }
32 |
33 | const DebugVariables = () => {
34 | const [ varNameFilter, setVarNameFilter ] = useState('');
35 | const { atrament } = useAtrament();
36 | const translator = useTranslator();
37 | const inkVariables = listInkVariables(atrament).filter((v) => v[0].includes(varNameFilter));
38 |
39 | const handleFilterChange = (e) => {
40 | setVarNameFilter(e.target.value);
41 | };
42 |
43 | return(
44 |
45 |
46 |
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default DebugVariables;
60 |
--------------------------------------------------------------------------------
/src/components/views/debugger/visits.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useAtrament } from 'src/atrament/hooks';
3 | import { useTranslator } from '@eo-locale/preact';
4 |
5 | import Collapse from 'src/components/ui/collapse';
6 | import Table from 'src/components/ui/table';
7 |
8 | function listInkVisits(atrament) {
9 | const visitCounts = atrament.ink.story().state._visitCounts;
10 | const inkVisitCounts = [];
11 | for (let item of visitCounts) {
12 | inkVisitCounts.push(item);
13 | }
14 | return inkVisitCounts.sort((a, b) => a[0] > b[0] ? 0 : -1);
15 | }
16 |
17 | const DebugVisits = () => {
18 | const { atrament } = useAtrament();
19 | const translator = useTranslator();
20 | const inkVisits = listInkVisits(atrament);
21 | return(
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default DebugVisits;
29 |
--------------------------------------------------------------------------------
/src/components/views/home-menu/game-cover.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useTranslator } from '@eo-locale/preact';
3 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
4 |
5 | import ContainerImage from 'src/components/ui/container-image';
6 | import Header from 'src/components/ui/header';
7 |
8 | const GameCover = () => {
9 | const { getAssetPath } = useAtrament();
10 | const translator = useTranslator();
11 | const atramentState = useAtramentState(['metadata']);
12 | const { title, author, cover } = atramentState.metadata;
13 | return (
14 |
19 | )
20 | };
21 |
22 | export default GameCover;
23 |
--------------------------------------------------------------------------------
/src/components/views/home-menu/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useEffect, useState } from 'preact/hooks';
3 | import { route } from 'preact-router';
4 | import { Text } from '@eo-locale/preact';
5 |
6 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
7 |
8 | import Block from 'src/components/ui/block';
9 | import LinkMenu from 'src/components/ui/link-menu';
10 |
11 | import SessionsView from 'src/components/views/sessions';
12 |
13 | import LoadGameMenu from './load-game';
14 | import GameCover from './game-cover';
15 | import useGameControls from './use-game-controls';
16 |
17 |
18 | const AboutGame = () => ( route('/about')}>);
19 |
20 | const MainMenu = ({ canBeResumed, canBeLoaded, openLoadGameMenu, about }) => {
21 | const { newGame, resumeGame } = useGameControls();
22 | return (
23 | <>
24 |
25 |
26 | {canBeResumed ? : ''}
27 |
28 | {canBeLoaded ? : ''}
29 | {about ? : ''}
30 |
31 | >
32 | );
33 | };
34 |
35 |
36 | export const SessionsMenuView = () => {
37 | const [ loadGameMenuVisible, setLoadGameMenuVisible ] = useState(false);
38 | const { atrament, canResume } = useAtrament();
39 | const { newGame, resumeGame } = useGameControls();
40 | const { metadata } = useAtramentState(['metadata']);
41 |
42 | const openLoadGameMenu = () => setLoadGameMenuVisible(true);
43 | const closeLoadGameMenu = () => {
44 | atrament.game.setSession();
45 | setLoadGameMenuVisible(false);
46 | }
47 |
48 | return (
49 | <>
50 | {loadGameMenuVisible
51 | ?
52 | : <>
53 |
54 |
55 |
56 | {metadata.about ? : ''}
57 |
58 | >
59 | }
60 | >
61 | );
62 | };
63 |
64 |
65 | export const HomeMenuView = () => {
66 | const [ loadGameMenuVisible, setLoadGameMenuVisible ] = useState(false);
67 | const [ canBeResumed, setResumeState ] = useState(false);
68 | const [ canBeLoaded, setLoadedState ] = useState(false);
69 | const { atrament, canResume } = useAtrament();
70 | const { metadata } = useAtramentState(['metadata']);
71 |
72 | const openLoadGameMenu = () => setLoadGameMenuVisible(true);
73 | const closeLoadGameMenu = () => setLoadGameMenuVisible(false);
74 |
75 | useEffect(() => {
76 | const initHome = async () => {
77 | const canResumeGame = await canResume();
78 | setResumeState(!!canResumeGame);
79 | const existingSaves = await atrament.game.listSaves();
80 | const saves = existingSaves.filter(
81 | (s) => {
82 | if (metadata.load_from_checkpoints && s.type === atrament.game.SAVE_CHECKPOINT) {
83 | return true;
84 | }
85 | return s.type === atrament.game.SAVE_GAME;
86 | }
87 | );
88 | if (saves.length) {
89 | setLoadedState(true);
90 | }
91 | }
92 | initHome();
93 | }, [ atrament.game, canResume, metadata, setLoadedState ]);
94 |
95 | return (
96 | <>
97 | {loadGameMenuVisible
98 | ?
99 | :
100 | }
101 | >
102 | );
103 | };
104 |
105 |
--------------------------------------------------------------------------------
/src/components/views/home-menu/load-game.jsx:
--------------------------------------------------------------------------------
1 |
2 | import { h } from 'preact';
3 | import { Text } from '@eo-locale/preact';
4 |
5 | import Block from 'src/components/ui/block';
6 | import Header from 'src/components/ui/header';
7 | import Break from 'src/components/ui/break';
8 |
9 | import LoadGameView from 'src/components/views/loadgame';
10 |
11 | import useGameControls from './use-game-controls';
12 |
13 | const LoadGameMenu = ({ children }) => {
14 | const { loadGame } = useGameControls();
15 | return (
16 |
17 |
18 |
19 |
20 | {children}
21 |
22 | );
23 | };
24 |
25 | export default LoadGameMenu;
26 |
--------------------------------------------------------------------------------
/src/components/views/home-menu/use-game-controls.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'preact/hooks';
2 | import { route } from 'preact-router';
3 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
4 |
5 | const useGameControls = () => {
6 | const { resetBackground, gameStart, gameResume, continueStory } = useAtrament();
7 | const { metadata } = useAtramentState(['metadata']);
8 |
9 | const newGame = useCallback(async () => {
10 | resetBackground();
11 | await gameStart();
12 | continueStory();
13 | route('/game');
14 | }, [ resetBackground, gameStart, continueStory ]);
15 |
16 | const resumeGame = useCallback(async () => {
17 | resetBackground();
18 | await gameResume();
19 | if (metadata.continue_maximally !== false) {
20 | continueStory();
21 | }
22 | route('/game');
23 | }, [ resetBackground, gameResume, continueStory, metadata ]);
24 |
25 | const loadGame = useCallback(async (saveslot) => {
26 | resetBackground();
27 | await gameStart(saveslot);
28 | if (metadata.continue_maximally !== false) {
29 | continueStory();
30 | }
31 | route('/game');
32 | }, [ resetBackground, gameStart, continueStory, metadata ]);
33 |
34 | return { newGame, resumeGame, loadGame };
35 | };
36 |
37 | export default useGameControls;
38 |
--------------------------------------------------------------------------------
/src/components/views/loadgame/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useTranslator } from '@eo-locale/preact';
3 | import { useEffect, useState, useCallback } from 'preact/hooks';
4 |
5 | import { appLocale } from 'src/constants';
6 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
7 |
8 | import MenuListItem from 'src/components/ui/menu-list-item';
9 |
10 | const datefmt = (date) => new Date(date).toLocaleString(appLocale);
11 |
12 |
13 | const LoadGameView = ({ loadGame, hasConfirmation = false }) => {
14 | const [ saveslots, setSaveslots ] = useState([]);
15 | const translator = useTranslator();
16 | const { atrament } = useAtrament();
17 | const { metadata } = useAtramentState(['metadata']);
18 |
19 | const initLoadgame = useCallback(async () => {
20 | const existingSaves = await atrament.game.listSaves();
21 | const saveSlotList = [];
22 | // autosave
23 | const autosave = existingSaves
24 | .filter((s) => s.type === atrament.game.SAVE_AUTOSAVE)
25 | .map((s) => ({ ...s, slot: translator.translate('main.autosave', { date: datefmt(s.date) }) }));
26 | if (autosave.length) {
27 | saveSlotList.push(autosave[0]);
28 | }
29 | // checkpoints
30 | if (metadata.load_from_checkpoints) {
31 | const checkpoints = existingSaves
32 | .filter((s) => s.type === atrament.game.SAVE_CHECKPOINT)
33 | .map((s) => ({
34 | ...s,
35 | slot: typeof s.name === 'boolean'
36 | ? translator.translate('main.checkpoint')
37 | : translator.translate('main.checkpoint-named', { name: s.name })
38 | }));
39 | if (checkpoints.length) {
40 | saveSlotList.push(...checkpoints);
41 | }
42 | }
43 | // saved games
44 | const saves = existingSaves
45 | .filter((s) => s.type === atrament.game.SAVE_GAME)
46 | .map((s) => ({ ...s, slot: datefmt(s.date) }));
47 | saveSlotList.push(...saves);
48 | // done
49 | setSaveslots(saveSlotList);
50 | }, [ atrament, metadata, translator ]);
51 |
52 | const startSavedGame = useCallback(async (ev) => {
53 | const chosenSaveslot = ev.target.getAttribute('data-save');
54 | await loadGame(chosenSaveslot);
55 | }, [ loadGame ]);
56 |
57 | const deleteSavedGame = useCallback(async (ev) => {
58 | const chosenSaveslot = ev.target.getAttribute('data-save');
59 | await atrament.game.removeSave(chosenSaveslot);
60 | await initLoadgame();
61 | }, [ atrament, initLoadgame ]);
62 |
63 | useEffect(() => {
64 | const onLoad = async () => initLoadgame();
65 | onLoad();
66 | }, [ initLoadgame ]);
67 |
68 | return (
69 | <>
70 | {saveslots.map((s) =>
71 |
84 | {s.slot}
85 |
86 | )}
87 | >
88 | );
89 | };
90 |
91 | export default LoadGameView;
92 |
--------------------------------------------------------------------------------
/src/components/views/overlay/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | import { IconToolbarBack } from 'src/components/ui/icons';
5 |
6 | import { useAtramentOverlay } from 'src/atrament/hooks';
7 |
8 | import ContainerText from 'src/components/ui/container-text';
9 | import TextParagraph from 'src/components/ui/text-paragraph';
10 | import Markup from 'src/components/ui/markup';
11 |
12 | const OverlayView = () => {
13 | const { overlay, closeOverlay } = useAtramentOverlay();
14 |
15 | if (!overlay.current) {
16 | return <>>;
17 | }
18 |
19 | return (
20 |
21 |
25 |
26 |
27 | {overlay.content.map((item, index) => )}
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | export default OverlayView;
35 |
--------------------------------------------------------------------------------
/src/components/views/overlay/index.module.css:
--------------------------------------------------------------------------------
1 | .overlay_container {
2 | position: absolute;
3 | width: 100%;
4 | height: 100%;
5 | display: flex;
6 | flex-direction: column;
7 | background: var(--bg-color);
8 | z-index: 80;
9 | }
10 |
11 | .overlay_header {
12 | min-height: 2.5em;
13 | flex-grow: 0;
14 | flex-shrink: 0;
15 | flex-basis: auto;
16 | border-bottom: 1px solid var(--fg-color);
17 | display: flex;
18 | }
19 |
20 | .overlay_header > * {
21 | vertical-align: middle;
22 | }
23 |
24 | .overlay_title {
25 | flex-grow: 1;
26 | margin-right: 3rem;
27 | padding: 0.25rem;
28 | display: flex;
29 | align-items: center;
30 | justify-content: center;
31 | }
32 |
33 | .overlay_content {
34 | padding: 1rem;
35 | flex-grow: 1;
36 | overflow: auto;
37 | }
38 |
39 | .overlay_content p {
40 | margin-top: 0;
41 | }
42 |
43 | .overlay_content p ul {
44 | margin-top: 0;
45 | margin-bottom: 0;
46 | }
47 |
48 | .button_back {
49 | cursor: pointer;
50 | border: none;
51 | background: none;
52 | color: var(--font-color);
53 | flex-shrink: 0;
54 | width: 3rem;
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/views/savegame/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useTranslator } from '@eo-locale/preact';
3 | import { useEffect, useState, useCallback } from 'preact/hooks';
4 |
5 | import { appLocale } from 'src/constants';
6 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
7 |
8 | import MenuListItem from 'src/components/ui/menu-list-item';
9 |
10 | const datefmt = (date) => new Date(date).toLocaleString(appLocale);
11 |
12 | const SaveGameView = ({ saveGame }) => {
13 | const [ saveslots, setSaveslots ] = useState([]);
14 | const translator = useTranslator();
15 | const { atrament } = useAtrament();
16 | const { metadata } = useAtramentState(['metadata']);
17 | const numberOfSaveSlots = metadata.saves ? +metadata.saves : 0;
18 |
19 | const initSavegame = useCallback(async () => {
20 | const existingSaves = await atrament.game.listSaves();
21 | // saved games
22 | const saves = existingSaves
23 | .filter((s) => s.type === atrament.game.SAVE_GAME)
24 | .map((s) => ({ ...s, slot: datefmt(s.date) }));
25 | if (saves.length < numberOfSaveSlots) {
26 | // new save is possible
27 | saves.push({
28 | name: saves.length + 1,
29 | slot: translator.translate('main.new-save')
30 | });
31 | }
32 | setSaveslots(saves);
33 | }, [ atrament, translator, numberOfSaveSlots ]);
34 |
35 | const saveGameToSlot = useCallback(async (ev) => {
36 | const chosenSaveslot = ev.target.getAttribute('data-savename');
37 | await saveGame(chosenSaveslot);
38 | await initSavegame();
39 | }, [ saveGame, initSavegame ]);
40 |
41 | const deleteSavedGame = useCallback(async (ev) => {
42 | const chosenSaveslot = ev.target.getAttribute('data-saveid');
43 | await atrament.game.removeSave(chosenSaveslot);
44 | await initSavegame();
45 | }, [ atrament, initSavegame ]);
46 |
47 | useEffect(() => {
48 | const onLoad = async () => initSavegame();
49 | onLoad();
50 | }, [ initSavegame ]);
51 |
52 | return (
53 | <>
54 | {saveslots.map((s) =>
55 |
69 | {s.slot}
70 |
71 | )}
72 | >
73 | );
74 | };
75 |
76 | export default SaveGameView;
77 |
--------------------------------------------------------------------------------
/src/components/views/sessions/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Text, useTranslator } from '@eo-locale/preact';
3 | import { useEffect, useState, useCallback } from 'preact/hooks';
4 |
5 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
6 |
7 | import MenuListItem from 'src/components/ui/menu-list-item';
8 |
9 |
10 | const SessionsView = ({ newGame, loadGame, resumeGame, canResume }) => {
11 | const [ sessions, setSessions ] = useState([]);
12 | const translator = useTranslator();
13 | const { atrament } = useAtrament();
14 | const { metadata } = useAtramentState(['metadata']);
15 |
16 | const initSessions = useCallback(async () => {
17 | const existingSessions = await atrament.game.getSessions();
18 | let firstEmptySlot = true;
19 | setSessions([...Array(+metadata.sessions).keys()].map((s) => {
20 | const id = s + 1;
21 | const name = `session${id}`;
22 | const hasSaves = !!existingSessions[`session${id}`];
23 | const canStart = hasSaves || firstEmptySlot;
24 | if (!hasSaves && firstEmptySlot) {
25 | firstEmptySlot = false;
26 | }
27 | return { id, name, hasSaves, canStart };
28 | }));
29 | }, [ atrament, metadata, setSessions ]);
30 |
31 | const startSession = useCallback(async (ev) => {
32 | const chosenSession = ev.target.getAttribute('data-session');
33 | const sessionData = sessions.filter(item => item.name === chosenSession)[0];
34 | if (!sessionData) {
35 | return;
36 | }
37 | atrament.game.setSession(chosenSession);
38 | if (sessionData.hasSaves) {
39 | if (await canResume()) {
40 | await resumeGame();
41 | } else {
42 | await loadGame();
43 | }
44 | } else {
45 | await newGame();
46 | }
47 | }, [ atrament, sessions, newGame, canResume, resumeGame, loadGame ]);
48 |
49 | const deleteSession = useCallback(async (ev) => {
50 | const chosenSession = ev.target.getAttribute('data-session');
51 | await atrament.game.removeSession(chosenSession);
52 | await initSessions();
53 | }, [ atrament, initSessions ]);
54 |
55 | useEffect(() => {
56 | const onLoad = async () => initSessions();
57 | onLoad();
58 | }, [ initSessions ]);
59 |
60 |
61 | return (
62 | <>
63 | {sessions.map((s) =>
64 |
75 | {s.hasSaves
76 | ?
77 | : (s.canStart
78 | ?
79 | :
80 | )}
81 |
82 | )}
83 | >
84 | );
85 | };
86 |
87 | export default SessionsView;
88 |
--------------------------------------------------------------------------------
/src/components/views/settings/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useTranslator } from '@eo-locale/preact';
4 |
5 | import Collapse from '../../ui/collapse';
6 |
7 | import SettingsFullscreen from './settings-fullscreen';
8 | import SettingsSound from './settings-sound';
9 | import SettingsText from './settings-text';
10 | import SettingsFont from './settings-font';
11 | import SettingsTheme from './settings-theme';
12 | import SettingsAnimation from './settings-animation';
13 |
14 | const Settings = () => {
15 | const translator = useTranslator();
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default Settings;
31 |
--------------------------------------------------------------------------------
/src/components/views/settings/index.module.css:
--------------------------------------------------------------------------------
1 | .settings_container {
2 | padding-top: 1em;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: flex-start;
6 | gap: 1em;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/views/settings/settings-animation/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Text } from '@eo-locale/preact';
3 | import Toggle from 'src/components/ui/toggle';
4 |
5 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
6 |
7 | const SettingsAnimation = () => {
8 | const { updateSettings } = useAtrament();
9 | const atramentState = useAtramentState(['settings']);
10 | const handleAnimation = (e) => updateSettings('animation', e.target.checked);
11 |
12 | return (
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default SettingsAnimation;
20 |
--------------------------------------------------------------------------------
/src/components/views/settings/settings-font/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
4 |
5 | import { fonts } from 'src/fonts';
6 |
7 | const SettingsFont = () => {
8 | const { updateSettings } = useAtrament();
9 | const atramentState = useAtramentState(['settings']);
10 | const handleFont = (e) => updateSettings('font', e.target.value);
11 | const font = atramentState.settings.font;
12 | return (
13 |
14 |
19 |
20 | );
21 | };
22 |
23 | export default SettingsFont;
24 |
--------------------------------------------------------------------------------
/src/components/views/settings/settings-font/index.module.css:
--------------------------------------------------------------------------------
1 | .settings_font_container {
2 | width: 100%;
3 | }
4 |
5 | .settings_font_container select {
6 | width: 100%;
7 | padding: 0.5rem;
8 | background-color: var(--bg-color);
9 | color: var(--fg-color);
10 | border: 1px solid var(--fg-color);
11 | border-radius: var(--border-radius);
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/views/settings/settings-fullscreen/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Text } from '@eo-locale/preact';
3 | import Toggle from 'src/components/ui/toggle';
4 |
5 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
6 |
7 | const SettingsFullscreen = () => {
8 | const { updateSettings } = useAtrament();
9 | const atramentState = useAtramentState(['settings']);
10 | const handleFullscreen = (e) => updateSettings('fullscreen', e.target.checked);
11 |
12 | return (
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default SettingsFullscreen;
20 |
--------------------------------------------------------------------------------
/src/components/views/settings/settings-sound/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Text } from '@eo-locale/preact';
3 | import style from './index.module.css';
4 | import Toggle from 'src/components/ui/toggle';
5 |
6 | import { IconVolumeLow, IconVolumeHigh } from 'src/components/ui/icons';
7 |
8 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
9 |
10 | const SettingsSound = () => {
11 | const { updateSettings } = useAtrament();
12 | const atramentState = useAtramentState(['settings']);
13 | const handleMute = (e) => updateSettings('mute', !e.target.checked);
14 | const handleVolume = (e) => updateSettings('volume', e.target.value);
15 | const { mute, volume } = atramentState.settings;
16 |
17 | return (
18 | <>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
34 |
35 |
36 |
37 | >
38 | );
39 | };
40 |
41 | export default SettingsSound;
42 |
--------------------------------------------------------------------------------
/src/components/views/settings/settings-sound/index.module.css:
--------------------------------------------------------------------------------
1 | .settings_sound_container {
2 | display: flex;
3 | flex-direction: row;
4 | }
5 |
6 | .settings_sound_input_container {
7 | flex-grow: 1;
8 | }
9 |
10 | .settings_sound_volume {
11 | width: 100%;
12 | margin-top: 0.1rem;
13 | vertical-align: top;
14 | accent-color: var(--fg-color);
15 | }
16 |
17 | .settings_sound_icon {
18 | padding-left: 0.5rem;
19 | padding-right: 0.5rem;
20 | height: 1.2rem;
21 | min-width: 2.5rem;
22 | width: 2.5rem;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/views/settings/settings-text/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Text } from '@eo-locale/preact';
3 | import style from './index.module.css';
4 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
5 |
6 | import { defaultFontSize, stepFontSize, minFontSize, maxFontSize } from 'src/constants';
7 |
8 | import { fonts } from 'src/fonts';
9 |
10 | const datapointsFontSize = [];
11 | for (let s=minFontSize; s <= maxFontSize; s+= stepFontSize) {
12 | datapointsFontSize.push(s);
13 | }
14 |
15 | const SettingsText = () => {
16 | const { updateSettings } = useAtrament();
17 | const atramentState = useAtramentState(['settings']);
18 | const { font, fontSize } = atramentState.settings;
19 |
20 | const handleFontSize = (e) => updateSettings('fontSize', e.target.value);
21 |
22 | return (
23 |
24 |
25 |
A
26 |
27 |
37 |
40 |
41 |
A
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default SettingsText;
51 |
--------------------------------------------------------------------------------
/src/components/views/settings/settings-text/index.module.css:
--------------------------------------------------------------------------------
1 | .settings_fontsize_container {
2 | display: flex;
3 | flex-direction: row;
4 | }
5 |
6 | .settings_font_a {
7 | margin: auto;
8 | min-width: 2rem;
9 | text-align: center;
10 | }
11 |
12 | .settings_fontsize_input_container {
13 | flex-grow: 1;
14 | }
15 |
16 | .settings_fontsize_input {
17 | width: 100%;
18 | margin-top: 0.5rem;
19 | accent-color: var(--fg-color);
20 | }
21 |
22 | .settings_font_sample {
23 | height: 9rem;
24 | padding: 0.5rem;
25 | line-height: calc(1ex / 0.32);
26 | overflow: hidden;
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/views/settings/settings-theme/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Text } from '@eo-locale/preact';
3 | import style from './index.module.css';
4 | import { useAtrament } from 'src/atrament/hooks';
5 |
6 | import { themes } from 'src/themes';
7 |
8 | const SettingButton = ({themeId, themeConfig, onClick }) => {
9 | const buttonStyle={
10 | color: themeConfig['fg-color'],
11 | 'background-color': themeConfig['bg-color']
12 | }
13 | return (
14 |
17 | );
18 | };
19 |
20 | const SettingsTheme = () => {
21 | const { updateSettings } = useAtrament();
22 | const handleTheme = (e) => updateSettings('theme', e.target.attributes['data-theme-id'].value);
23 | return (
24 |
25 | {Object.entries(themes).map(([k, v]) => {
26 | return
27 | })}
28 |
29 | );
30 | };
31 |
32 | export default SettingsTheme;
33 |
--------------------------------------------------------------------------------
/src/components/views/settings/settings-theme/index.module.css:
--------------------------------------------------------------------------------
1 | .settings_theme_container {
2 | display: flex;
3 | flex-direction: row;
4 | flex-wrap: wrap;
5 | gap: 0.5rem;
6 | }
7 |
8 | .settings_theme_button {
9 | display: block;
10 | padding: 0.75em;
11 | flex: 20%;
12 | text-align: center;
13 | text-decoration: none;
14 | border: 1px solid var(--fg-color);
15 | border-radius: var(--border-radius);
16 | cursor: pointer;
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/views/story-error/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useEffect, useCallback } from 'preact/hooks';
3 | import { ERROR_STORE_KEY } from 'src/constants';
4 | import { useAtrament, useAtramentState } from 'src/atrament/hooks';
5 |
6 | import ErrorModal from 'src/components/ui/error-modal';
7 |
8 | const StoryError = () => {
9 | const { atrament } = useAtrament();
10 | const atramentState = useAtramentState([ERROR_STORE_KEY]);
11 |
12 | const closeModal = useCallback(() => atrament.state.setKey(ERROR_STORE_KEY, null), [ atrament ]);
13 |
14 | const escHandler = useCallback((e) => {
15 | if (e.key === "Escape") {
16 | closeModal()
17 | }
18 | }, [ closeModal ]);
19 |
20 | useEffect(() => {
21 | document.addEventListener("keydown", escHandler, false);
22 | return () => {
23 | document.removeEventListener("keydown", escHandler, false);
24 | }
25 | }, [ escHandler ]);
26 |
27 | if (atramentState[ERROR_STORE_KEY]) {
28 | return ;
29 | }
30 | };
31 |
32 | export default StoryError;
--------------------------------------------------------------------------------
/src/components/views/story/choice-button-group/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useState, useCallback, useEffect } from 'preact/hooks';
3 | import { useAtrament } from 'src/atrament/hooks';
4 | import ChoiceButton from '../choice-button';
5 |
6 | const ChoiceButtonGroup = ({ key, currentScene, setReady }) => {
7 | const { makeChoice, continueStory } = useAtrament();
8 | const [ chosen, setChosen ] = useState(null);
9 |
10 | const numberOfChoices = (currentScene && currentScene.choices) ? currentScene.choices.length : -1;
11 |
12 | const selectChoice = useCallback((id) => {
13 | const delay = numberOfChoices > 1 ? 350 : 150;
14 | setChosen(id);
15 | setTimeout(() => {
16 | // pass choice to Atrament
17 | setTimeout(() => {
18 | setChosen(null);
19 | setReady(false);
20 | makeChoice(id);
21 | continueStory();
22 | }, delay);
23 | }, 0);
24 | }, [ makeChoice, continueStory, setReady, numberOfChoices ]);
25 |
26 | const kbdChoiceHandler = useCallback((e) => {
27 | const kbdChoice = +e.key;
28 | const targetElement = e.target.tagName.toLowerCase();
29 | if (targetElement === 'input') {
30 | // ignore keyboard event
31 | return;
32 | }
33 | if (
34 | numberOfChoices > 0 &&
35 | kbdChoice > 0 &&
36 | kbdChoice <= numberOfChoices &&
37 | !currentScene.choices[kbdChoice-1].disabled
38 | ) {
39 | selectChoice(currentScene.choices[kbdChoice-1].id);
40 | }
41 | }, [ numberOfChoices, selectChoice, currentScene.choices ]);
42 |
43 | useEffect(() => {
44 | document.addEventListener("keydown", kbdChoiceHandler, false);
45 | return () => {
46 | document.removeEventListener("keydown", kbdChoiceHandler, false);
47 | }
48 | }, [ kbdChoiceHandler ]);
49 |
50 | return (
51 | <>
52 | {currentScene.choices.map((choice, index) => (
53 | ))
59 | }
60 | >
61 | )
62 | };
63 |
64 | export default ChoiceButtonGroup;
65 |
--------------------------------------------------------------------------------
/src/components/views/story/choice-button/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | import { useAtramentState } from 'src/atrament/hooks';
5 | import Markup from 'src/components/ui/markup';
6 |
7 | const ChoiceButton = ({ choice, chosen, handleClick }) => {
8 | const { metadata } = useAtramentState(['metadata']);
9 |
10 | const choiceIsMade = chosen !== null; // something is chosen
11 | const activeChoice = chosen === choice.id; // this is the active choice
12 | const onClick = () => {
13 | if (!choiceIsMade) {
14 | handleClick(choice.id);
15 | }
16 | };
17 |
18 | const choiceStateClass = choiceIsMade ? (activeChoice ? style.choice_active : style.choice_inactive) : '';
19 | const choiceGroupStyle = metadata.choices?.includes('grouped') ? style.buttons_grouped : style.buttons_separate;
20 | const choiceAlignment = metadata.choices?.includes('left') ? style.left_aligned : metadata.choices?.includes('right') ? style.right_aligned : '';
21 |
22 |
23 | let choiceCustomClass = choice.tags.CLASS;
24 | if (Array.isArray(choiceCustomClass)) {
25 | choiceCustomClass = choiceCustomClass.join(' ');
26 | }
27 |
28 | const elementClasses = [style.choice_button, choiceGroupStyle, choiceAlignment, choiceStateClass, choiceCustomClass ];
29 |
30 | return (
31 |
39 | );
40 | };
41 |
42 | export default ChoiceButton;
43 |
--------------------------------------------------------------------------------
/src/components/views/story/choice-button/index.module.css:
--------------------------------------------------------------------------------
1 | .choice_button {
2 | display: block;
3 | background-color: transparent;
4 | color: var(--fg-color);
5 | margin-top: 0.5em;
6 | padding: 0.75em;
7 | text-align: center;
8 | text-decoration: none;
9 | width: 100%;
10 | cursor: pointer;
11 | }
12 |
13 | .choice_button:hover {
14 | background-color: var(--fg-color);
15 | color: var(--bg-color);
16 | }
17 |
18 | .choice_button:disabled {
19 | background-color: var(--shade-color);
20 | color: var(--fg-color);
21 | opacity: 0.6;
22 | cursor: inherit;
23 | }
24 |
25 | .choice_active {
26 | background-color: var(--fg-color);
27 | border: 1px solid var(--fg-color);
28 | color: var(--bg-color);
29 | }
30 |
31 | .choice_active:hover {
32 | background-color: var(--fg-color);
33 | border: 1px solid var(--fg-color);
34 | color: var(--bg-color);
35 | }
36 |
37 | @keyframes disappear {
38 | 0% {
39 | opacity: 1;
40 | }
41 | 90% {
42 | opacity: 0.3;
43 | }
44 | 100% {
45 | opacity: 0;
46 | }
47 | }
48 |
49 | .choice_inactive {
50 | filter: blur(4px);
51 | animation-name: disappear;
52 | animation-duration: var(--animation-disabled, 0.2s);
53 | animation-delay: 0s;
54 | animation-iteration-count: 1;
55 | animation-fill-mode: forwards;
56 | }
57 |
58 | .choice_inactive:hover {
59 | background-color: var(--bg-color);
60 | color: var(--fg-color);
61 | }
62 |
63 | .choice_active:disabled:hover {
64 | background-color: var(--shade-color);
65 | color: var(--fg-color);
66 | }
67 |
68 |
69 | /* choice buttons - separate */
70 |
71 | .buttons_separate {
72 | border: 1px solid var(--fg-color);
73 | border-radius: var(--border-radius);
74 | }
75 |
76 | /* choice buttons - block-like */
77 |
78 | .buttons_grouped {
79 | margin-top: 0;
80 | border: 1px solid var(--fg-color);
81 | border-bottom: none;
82 | }
83 |
84 | .buttons_grouped:first-child {
85 | margin-top: 0.5em;
86 | border-radius: var(--border-radius) var(--border-radius) 0 0;
87 | border-bottom: none;
88 | }
89 |
90 | .buttons_grouped:last-child {
91 | border-radius: 0 0 var(--border-radius) var(--border-radius);
92 | border-bottom: 1px solid var(--fg-color);
93 | }
94 |
95 | .buttons_grouped:only-child {
96 | border-radius: var(--border-radius);
97 | }
98 |
99 |
100 | .left_aligned {
101 | text-align: left;
102 | }
103 |
104 | .right_aligned {
105 | text-align: right;
106 | }
--------------------------------------------------------------------------------
/src/components/views/story/choices/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Text } from '@eo-locale/preact';
3 | import { route } from 'preact-router';
4 | import { useAtrament } from 'src/atrament/hooks';
5 | import LinkHome from 'src/components/ui/link-home';
6 | import ChoiceButtonGroup from '../choice-button-group';
7 | import ClickToContinue from '../click-to-continue';
8 |
9 | import getTagAttributes from 'src/utils/get-tag-attributes';
10 |
11 | function clickToContinueOptions(param) {
12 | const options = {
13 | delay: 0,
14 | animation: 0,
15 | clickable: 0
16 | };
17 | if (param.startsWith('(')) {
18 | const attrs = getTagAttributes(param.replace(/[()]/g, ''));
19 | Object.entries(attrs).forEach(([k,v]) => options[k] = v);
20 | // alternate syntax
21 | if (options.continue) {
22 | options.delay = options.continue;
23 | }
24 | } else if (param) {
25 | options.delay = param;
26 | } else {
27 | options.animation = 3; // default animation pause
28 | }
29 | return options;
30 | }
31 |
32 |
33 | const EndGameLink = () => {
34 | const { atrament } = useAtrament();
35 | const endGame = async () => {
36 | await atrament.game.removeSave();
37 | route('/');
38 | };
39 | return ();
40 | };
41 |
42 | const Choices = ({ key, currentScene, setReady, isHypertextMode }) => {
43 | const numberOfChoices = (currentScene && currentScene.choices) ? currentScene.choices.length : -1;
44 | if (numberOfChoices === 1) {
45 | const isClickToContinue = currentScene.choices[0].choice.match(/^(|>>>(\d*|\(.+?\)))$/);
46 | if (isClickToContinue) {
47 | // click-to-continue choice
48 | const options = clickToContinueOptions(isClickToContinue[2]);
49 | return ();
56 | }
57 | } else if (numberOfChoices === 0) {
58 | if (currentScene.canContinue) {
59 | // paragraph mode
60 | return ();
61 | }
62 | // end game
63 | return ();
64 | }
65 | if (isHypertextMode) {
66 | return <>>;
67 | }
68 | return (
69 |
74 | );
75 | };
76 |
77 | export default Choices;
78 |
--------------------------------------------------------------------------------
/src/components/views/story/click-to-continue/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useCallback, useEffect, useState } from 'preact/hooks';
4 | import { useTranslator } from '@eo-locale/preact';
5 | import { useAtrament } from 'src/atrament/hooks';
6 |
7 | const getSceneElement = () => document.getElementsByClassName('atrament-container-scene')[0];
8 | const getChoicesElement = () => document.getElementsByClassName('atrament-container-choices')[0];
9 |
10 | // options
11 | // clickable: pause before "click-to-continue" is allowed (0 - immediately)
12 | // animation: pause before animation is displayed (0 - immediately)
13 | // delay: pause before story continues (0 - only after click)
14 |
15 | const ClickToContinue = ({ setReady, withChoice = false, delay = 0, animation = 0, clickable = 0 }) => {
16 | const { makeChoice, continueStory } = useAtrament();
17 | const [ isVisible, setIsVisible ] = useState(false);
18 | const translator = useTranslator();
19 |
20 | const continueGame = useCallback(() => {
21 | setTimeout(() => {
22 | setReady(false);
23 | if (withChoice) {
24 | makeChoice(0);
25 | }
26 | continueStory();
27 | }, 0);
28 | }, [ makeChoice, continueStory, setReady, withChoice ]);
29 |
30 | const kbdChoiceHandler = useCallback((e) => {
31 | if (e.key === ' ' || e.key === 'Enter') {
32 | continueGame();
33 | }
34 | }, [ continueGame ]);
35 |
36 | const addEventListeners = useCallback(() => {
37 | document.addEventListener("keydown", kbdChoiceHandler, false);
38 | getSceneElement().addEventListener("click", continueGame, false);
39 | getChoicesElement().addEventListener("click", continueGame, false);
40 | return 0;
41 | }, [ continueGame, kbdChoiceHandler ]);
42 |
43 | const removeEventListeners = useCallback(() => {
44 | document.removeEventListener("keydown", kbdChoiceHandler, false);
45 | getSceneElement().removeEventListener("click", continueGame, false);
46 | getChoicesElement().removeEventListener("click", continueGame, false);
47 | return 0;
48 | }, [ continueGame, kbdChoiceHandler ]);
49 |
50 | useEffect(() => {
51 | const delayClickToContinue = setTimeout(addEventListeners, clickable * 1000);
52 | const delayAnimation = setTimeout(() => setIsVisible(true), animation * 1000);
53 | const delayChoice = delay ? setTimeout(continueGame, delay * 1000) : undefined;
54 | return () => {
55 | clearTimeout(delayAnimation);
56 | clearTimeout(delayClickToContinue);
57 | clearTimeout(delayChoice);
58 | removeEventListeners();
59 | }
60 | }, [ addEventListeners, removeEventListeners, continueGame, setIsVisible, clickable, delay, animation ]);
61 |
62 | const elementStyles = [style.circle];
63 | if (delay) {
64 | elementStyles.push(style.circle_empty);
65 | }
66 |
67 | return (
68 |
69 | {isVisible
70 | ?
71 | : ''
72 | }
73 |
74 | )
75 | };
76 |
77 | export default ClickToContinue;
--------------------------------------------------------------------------------
/src/components/views/story/click-to-continue/index.module.css:
--------------------------------------------------------------------------------
1 | @keyframes circle {
2 | from {
3 | transform: scale(0)
4 | }
5 | to {
6 | transform: scale(1)
7 | }
8 | }
9 |
10 | .container {
11 | position: relative;
12 | height: 3em;
13 | max-height: 3em;
14 | cursor: pointer;
15 | overflow: hidden;
16 | }
17 |
18 | .circle {
19 | position: absolute;
20 | top: 50%;
21 | left: 50%;
22 | transform: translate(-50%, -50%); /* center image */
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | width: 1.5em;
27 | height: 1.5em;
28 | background-color: var(--fg-color);
29 | border-radius: 50%;
30 | opacity: 0.3;
31 | }
32 |
33 | .circle:before,
34 | .circle:after {
35 | position: absolute;
36 | content: "";
37 | top: 0;
38 | right: 0;
39 | bottom: 0;
40 | left: 0;
41 | border: solid 3px var(--fg-color);
42 | border-radius: 50%;
43 | }
44 |
45 | .circle:before {
46 | animation: ripple 4s linear infinite;
47 | }
48 |
49 | .circle:after {
50 | animation: ripple 2s linear infinite;
51 | }
52 |
53 | .circle_empty {
54 | background-color: inherit;
55 | }
56 |
57 | @keyframes ripple {
58 | to {
59 | transform: scale(2);
60 | opacity: 0;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/views/story/container-choices/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | // UI
4 | import Block from 'src/components/ui/block';
5 |
6 | const ContainerChoices = ({ children, key, isReady }) => {
7 | return (
8 |
9 |
10 |
11 | {children}
12 |
13 |
14 |
15 | )
16 | };
17 |
18 | export default ContainerChoices;
--------------------------------------------------------------------------------
/src/components/views/story/container-choices/index.module.css:
--------------------------------------------------------------------------------
1 | .container_choices {
2 | flex-grow: 0;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/views/story/container-scenes/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | const getAlign = (v) => {
5 | const alignments = {
6 | top: 'start',
7 | start: 'start',
8 | middle: 'center',
9 | bottom: 'end',
10 | end: 'end'
11 | };
12 | return alignments[v] || 'center';
13 | }
14 |
15 | const ContainerScenes = ({ children, align }) => {
16 | return (
17 |
18 | {children}
19 |
20 | );
21 | };
22 |
23 | export default ContainerScenes;
24 |
--------------------------------------------------------------------------------
/src/components/views/story/container-scenes/index.module.css:
--------------------------------------------------------------------------------
1 | .container_scenes {
2 | flex-grow: 1;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/views/story/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useState } from 'preact/hooks';
3 |
4 | import { useAtramentState } from 'src/atrament/hooks';
5 |
6 | import ContainerText from 'src/components/ui/container-text';
7 | import ContainerScenes from './container-scenes';
8 | import ContainerChoices from './container-choices';
9 |
10 | import Scene from './scene';
11 | import Choices from './choices';
12 |
13 | const StoryView = () => {
14 | const atramentState = useAtramentState(['scenes', 'metadata']);
15 | const [ isReady, setReady ] = useState(false);
16 |
17 | const lastSceneIndex = atramentState.scenes.length - 1;
18 | const isHypertextMode = !!atramentState.metadata.hypertext;
19 | const key = `choices-${atramentState.scenes[lastSceneIndex]?.uuid}`;
20 |
21 | return (
22 |
23 |
24 | {atramentState.scenes.map((s, i) =>
25 |
32 | )}
33 |
34 |
35 |
41 |
42 |
43 | )
44 | }
45 |
46 | export default StoryView;
47 |
--------------------------------------------------------------------------------
/src/components/views/story/scene-paragraph/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 |
4 | import Markup from 'src/components/ui/markup';
5 |
6 | const Paragraph = ( {content, isCurrent} ) => {
7 | if (content.text === "\n") {
8 | return <>>;
9 | }
10 |
11 | let pStyle;
12 | if (!isCurrent) {
13 | pStyle = {opacity: '70%'};
14 | }
15 |
16 | let pClass = content.tags.CLASS || '';
17 | if (Array.isArray(pClass)) {
18 | pClass = pClass.join(' ');
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | export default Paragraph;
29 |
--------------------------------------------------------------------------------
/src/components/views/story/scene-paragraph/index.module.css:
--------------------------------------------------------------------------------
1 | .paragraph {
2 | display: block;
3 | margin-block-start: 1em;
4 | margin-block-end: 1em;
5 | margin-inline-start: 0;
6 | margin-inline-end: 0;
7 | unicode-bidi: isolate;
8 | }
9 |
10 | .paragraph:first-child {
11 | margin-block-start: 0;
12 | }
13 |
14 | .paragraph:last-child {
15 | margin-block-end: 0;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/views/story/scene/index.jsx:
--------------------------------------------------------------------------------
1 | import { h, Fragment } from 'preact';
2 | import style from './index.module.css';
3 | import { useRef, useState, useEffect, useCallback } from 'preact/hooks';
4 | import { scrollIntoView } from "seamless-scroll-polyfill";
5 | // UI
6 | import ContainerImage from 'src/components/ui/container-image';
7 | import Paragraph from '../scene-paragraph';
8 | // utils
9 | import { useAtrament } from 'src/atrament/hooks';
10 | import preloadImages from 'src/utils/preload-images';
11 |
12 | const Scene = ({ scene, isCurrent, isSingle, readyHandler }) => {
13 | const { getAssetPath } = useAtrament();
14 | const [ isLoaded, setIsLoaded ] = useState(false);
15 | const elementRef = useRef(null);
16 |
17 | const scrollToScene = useCallback((behavior) => {
18 | readyHandler(true);
19 | setTimeout(
20 | () => {
21 | // elementRef.current.scrollIntoView({ behavior, block: 'start' });
22 | // native 'scrollIntoView' glitches sometimes, so we use polyfill instead
23 | scrollIntoView(elementRef.current, { behavior, block: 'start' }, { duration: 300 });
24 | },
25 | 100
26 | );
27 | }, [ readyHandler ]);
28 |
29 | useEffect(() => {
30 | if (isLoaded && isCurrent) {
31 | scrollToScene(isSingle ? 'instant' : 'smooth');
32 | }
33 | }, [isLoaded, isCurrent, isSingle, scrollToScene]);
34 |
35 | // preload all images for scene
36 | useEffect(() => {
37 | const preloader = async () => {
38 | await preloadImages(getAssetPath, scene.images);
39 | setIsLoaded(true);
40 | }
41 | preloader();
42 | }, [ scene, setIsLoaded, getAssetPath ]);
43 |
44 | return (
45 |
46 |
47 | {
48 | scene.content.map((item, i) => (
49 |
50 | {item.images.map(
51 | (img, i) =>
52 | )}
53 |
54 |
55 | ))
56 | }
57 |
58 |
59 | )
60 | };
61 |
62 | export default Scene;
63 |
--------------------------------------------------------------------------------
/src/components/views/story/scene/index.module.css:
--------------------------------------------------------------------------------
1 | .scene {
2 | display: block;
3 | padding: 1rem;
4 | }
5 |
6 | .scene p:first-child {
7 | margin-top: 0px;
8 | }
9 |
10 | .scene p:last-child {
11 | margin-bottom: 0px;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/views/toolbar/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import style from './index.module.css';
3 | import { useTranslator } from '@eo-locale/preact';
4 | import { useAtramentState } from 'src/atrament/hooks';
5 | import Markup from 'src/components/ui/markup';
6 | import { TOOLBAR_STORE_KEY, TOOLBAR_DEFAULT } from 'src/constants';
7 |
8 | const Toolbar = () => {
9 | const translator = useTranslator();
10 | const atramentState = useAtramentState([TOOLBAR_STORE_KEY]);
11 | const toolbarContent = atramentState[TOOLBAR_STORE_KEY] !== TOOLBAR_DEFAULT
12 | ? atramentState[TOOLBAR_STORE_KEY]
13 | : translator.translate('default.title');
14 |
15 | return (
16 |
17 |
18 |
19 | )
20 | };
21 |
22 | export default Toolbar;
23 |
--------------------------------------------------------------------------------
/src/components/views/toolbar/index.module.css:
--------------------------------------------------------------------------------
1 | .toolbar {
2 | min-height: 2.5em;
3 | overflow: hidden;
4 | padding: 0.25em;
5 | padding-left: 0.5em;
6 | padding-right: 3rem;
7 | flex-grow: 0;
8 | flex-shrink: 0;
9 | flex-basis: auto;
10 | border-bottom: 1px solid var(--fg-color);
11 | font-family: var(--font-game);
12 | display: flex;
13 | align-items: center;
14 | justify-content: left;
15 | gap: 0.25rem;
16 | flex-wrap: wrap;
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | /* global __APP_VERSION__, __INK_SCRIPT__, __EMBED_FONTS__ */
2 |
3 | import cfg from '../atrament.config.json';
4 |
5 | export const appVersion = __APP_VERSION__;
6 |
7 | export const embedFonts = __EMBED_FONTS__;
8 |
9 | // Ink file
10 | export const gamePath = import.meta.env.MODE === 'production' && cfg.game?.zip
11 | ? cfg.game?.zip // only for production build, not available for "development" and "singlefile"
12 | : cfg.game?.path;
13 | export const gameFile = __INK_SCRIPT__;
14 |
15 | // Application ID
16 | // - uses page URL to make sure it's unique
17 |
18 | export const applicationID = [
19 | 'Atrament://',
20 | window.location.host,
21 | window.location.pathname,
22 | gamePath,
23 | '/',
24 | gameFile
25 | ].join('');
26 |
27 | //// Settings ////
28 |
29 | // i18n
30 | export const appLanguage = cfg.language || 'en';
31 | export const appLocale = cfg.locale || 'en-US';
32 |
33 | // theme
34 | export const gameDefaultTheme = cfg.theme;
35 |
36 | // font
37 | export const gameDefaultFont = cfg.font;
38 |
39 | // Font size range and step (percentage)
40 | export const defaultFontSize = 100;
41 | export const stepFontSize = 10;
42 | export const minFontSize = defaultFontSize - ( stepFontSize * 3);
43 | export const maxFontSize = defaultFontSize + ( stepFontSize * 5);
44 |
45 |
46 | //// Internal constants ////
47 |
48 | // error
49 | export const ERROR_STORE_KEY = 'ERROR';
50 |
51 | // overlay
52 | export const OVERLAY_STORE_KEY = 'OVERLAY';
53 |
54 | // toolbar
55 | export const TOOLBAR_STORE_KEY = 'TOOLBAR';
56 | export const TOOLBAR_DEFAULT = '__DEFAULT__';
57 |
58 | // known global tags (for debugger)
59 |
60 | export const KNOWN_GLOBAL_TAGS = [
61 | 'title',
62 | 'author',
63 | 'theme',
64 | 'font',
65 | 'observe',
66 | 'persist',
67 | 'sessions',
68 | 'autosave',
69 | 'saves',
70 | 'load_from_checkpoints',
71 | 'continue_maximally',
72 | 'single_scene',
73 | 'scenes_align',
74 | 'choices',
75 | 'hypertext',
76 | 'toolbar',
77 | 'about',
78 | 'cover',
79 | 'background',
80 | 'debug'
81 | ];
82 |
--------------------------------------------------------------------------------
/src/context.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'preact';
2 |
3 | export const AtramentContext = createContext(null);
4 | export const ActiveContentContext = createContext(true);
5 |
--------------------------------------------------------------------------------
/src/fonts.js:
--------------------------------------------------------------------------------
1 | // font styles
2 | import { gameDefaultFont, embedFonts } from 'src/constants';
3 |
4 | const emojiFonts = '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"';
5 |
6 | const systemFonts = {
7 | System: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, ${emojiFonts}`,
8 | 'Sans Serif': `"Trebuchet MS", "Lucida Grande", Arial, Helvetica, ${emojiFonts}`,
9 | Serif: `"Palatino", "Cambria", "Lucida Bright", "Georgia", "Times New Roman", ${emojiFonts}`,
10 | Monospaced: `"Monaco", "Consolas", "Lucida Console", "Courier New", ${emojiFonts}`
11 | }
12 |
13 | let fonts = systemFonts;
14 |
15 | (async () => {
16 | if (import.meta.env.MODE !== 'singlefile' || embedFonts) {
17 | // import font modules
18 | const extFonts = {};
19 | const modules = import.meta.glob('../resources/fonts/**/*.js');
20 | await Promise.all(
21 | Object.values(modules).map(
22 | (mod) => mod().then((fontmodule) => {
23 | const fnt = fontmodule.default;
24 | if (fnt.name) {
25 | extFonts[fnt.name] = `${fnt.name}, ${fnt.fallback || 'serif'}, ${emojiFonts}`;
26 | }
27 | })
28 | )
29 | );
30 | // add external fonts after the end of system fonts list
31 | Object.keys(extFonts).sort().forEach(fnt => (fonts[fnt] = extFonts[fnt]));
32 | }
33 | })();
34 |
35 | export {
36 | fonts
37 | };
38 |
39 | export function applyFont(font) {
40 | document.documentElement.style.setProperty('--font-game', fonts[font] || fonts[gameDefaultFont]);
41 | }
42 |
--------------------------------------------------------------------------------
/src/i18n.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "language": "en",
4 | "messages": {
5 | "yes": "Yes",
6 | "no": "No",
7 | "cancel": "Cancel",
8 | "back": "Go back",
9 | "default.title": "Atrament UI",
10 | "default.author": "Test Application",
11 | "main.menu": "Main menu",
12 | "main.newgame": "New game",
13 | "main.loadgame": "Load game",
14 | "main.savegame": "Save game",
15 | "main.continue": "Continue",
16 | "main.emptyslot": "Empty slot",
17 | "main.continue-session": "Continue game {session}",
18 | "main.autosave": "Autosave: {date}",
19 | "main.checkpoint": "Checkpoint",
20 | "main.checkpoint-named": "Checkpoint: {name}",
21 | "main.new-save": "New save",
22 | "main.delete-session": "Delete this session?",
23 | "main.delete-save": "Delete this saved game?",
24 | "main.confirm-load": "Load this game?",
25 | "main.confirm-save": "Overwrite this save?",
26 | "main.settings": "Settings",
27 | "main.about": "About",
28 | "main.exit": "Exit",
29 | "game.end": "The end",
30 | "game.save-and-quit": "Save game and quit",
31 | "game.quit": "Quit game",
32 | "game.click-to-continue": "Click to continue",
33 | "settings.fullscreen": "Fullscreen",
34 | "settings.animations": "Animations",
35 | "settings.sound": "Sound",
36 | "settings.appearance": "Appearance",
37 | "themes.light": "light",
38 | "themes.sepia": "sepia",
39 | "themes.dark": "dark",
40 | "font.sampleText": "The quick brown fox jumps over the lazy dog. Jackdaws love my big sphinx of quartz.",
41 | "debug": "Debug",
42 | "debug.info": "Info",
43 | "debug.info.ink-file": "Ink file",
44 | "debug.info.story-seed": "Story seed",
45 | "debug.info.current-turn-index": "Current turn index",
46 | "debug.info.path": "Path",
47 | "debug.global-tags": "Global tags",
48 | "debug.unknown-tag": "Unknown tag",
49 | "debug.variables": "Variables",
50 | "debug.variables.filter-by-name": "Filter by name",
51 | "debug.variables.name": "Name",
52 | "debug.variables.value": "Value",
53 | "debug.variables.cancel": "Cancel",
54 | "debug.variables.save": "Save",
55 | "debug.visits": "Visits",
56 | "debug.go-to-path": "Go to path",
57 | "debug.functions": "Functions",
58 | "debug.functions.arguments": "Arguments",
59 | "debug.functions.arguments_placeholder": "JSON-formatted agruments, i.e. [\"arg1\", 5]",
60 | "debug.functions.run-function": "Run function",
61 | "debug.functions.returned-value": "Returned value",
62 | "debug.functions.output": "Output",
63 | "about.components": "Components and libraries",
64 | "about.iftf_support": "Made with the support of the {iftf}"
65 | }
66 | },
67 | {
68 | "language": "ua",
69 | "messages": {
70 | "yes": "Так",
71 | "no": "Ні",
72 | "cancel": "Скасувати",
73 | "back": "Назад",
74 | "default.title": "Atrament UI",
75 | "default.author": "Тестовий застосунок",
76 | "main.menu": "Головне меню",
77 | "main.newgame": "Нова гра",
78 | "main.loadgame": "Завантажити гру",
79 | "main.savegame": "Зберегти гру",
80 | "main.continue": "Продовжити",
81 | "main.emptyslot": "Порожній слот",
82 | "main.continue-session": "Продовжити гру {session}",
83 | "main.autosave": "Автозбереження: {date}",
84 | "main.checkpoint": "Контрольна точка",
85 | "main.checkpoint-named": "Контрольна точка: {name}",
86 | "main.new-save": "Нове збереження",
87 | "main.delete-session": "Видалити сесію?",
88 | "main.delete-save": "Видалити збережену гру?",
89 | "main.confirm-load": "Завантажити цю гру?",
90 | "main.confirm-save": "Перезаписати це збереження?",
91 | "main.settings": "Налаштування",
92 | "main.about": "Про гру",
93 | "main.exit": "Вихід",
94 | "game.end": "Кінець",
95 | "game.save-and-quit": "Зберегти гру та вийти",
96 | "game.quit": "Вийти з гри",
97 | "game.click-to-continue": "Натисни, щоб продовжити",
98 | "settings.fullscreen": "На весь екран",
99 | "settings.animations": "Анімації",
100 | "settings.sound": "Звук",
101 | "settings.appearance": "Вигляд",
102 | "themes.light": "Світла",
103 | "themes.sepia": "Сепія",
104 | "themes.dark": "Темна",
105 | "font.sampleText": "Хвацький юшковар Філіп щодня на ґанку готує сім'ї вечерю з жаб.",
106 | "debug": "Налагодження",
107 | "debug.info": "Інформація",
108 | "debug.info.ink-file": "Ink-скрипт",
109 | "debug.info.story-seed": "RNG-сід історії",
110 | "debug.info.current-turn-index": "Індекс поточного ходу",
111 | "debug.info.path": "Поточний шлях",
112 | "debug.global-tags": "Глобальні теги",
113 | "debug.unknown-tag": "Невідомий тег",
114 | "debug.variables": "Змінні",
115 | "debug.variables.filter-by-name": "Фільтр за ім'ям",
116 | "debug.variables.name": "Ім'я",
117 | "debug.variables.value": "Значення",
118 | "debug.variables.cancel": "Скасувати",
119 | "debug.variables.save": "Зберегти",
120 | "debug.visits": "Візити",
121 | "debug.go-to-path": "Перейти до",
122 | "debug.functions": "Функції",
123 | "debug.functions.arguments": "Аргументи",
124 | "debug.functions.arguments_placeholder": "Аргументи у JSON-форматі, наприклад: [\"arg1\", 5]",
125 | "debug.functions.run-function": "Виконати функцію",
126 | "debug.functions.returned-value": "Повернене значення",
127 | "debug.functions.output": "Вивід",
128 | "about.components": "Компоненти та бібліотеки",
129 | "about.iftf_support": "Зроблено за підтримки фонду {iftf}"
130 | }
131 | }
132 | ]
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import { render } from 'preact';
2 |
3 | // CSS
4 | import 'normalize.css';
5 | import 'src/app.css';
6 | import '../resources/styles/custom.css';
7 |
8 | // App
9 | import App from './components/app';
10 |
11 | // Patches
12 | import 'src/utils/neutralino-out-of-bounds-fix';
13 |
14 | render(, document.getElementById('app'));
15 |
--------------------------------------------------------------------------------
/src/themes.js:
--------------------------------------------------------------------------------
1 | import { gameDefaultTheme } from 'src/constants';
2 |
3 | // import theme modules
4 | const themes = {};
5 |
6 | const allThemes = {};
7 | const modules = import.meta.glob('../resources/themes/*.json', { eager: true });
8 | Object.values(modules).map(({ default: theme }) => {
9 | if (theme) {
10 | allThemes[theme.name] = theme.theme;
11 | }
12 | });
13 | // add internal themes, if they are present
14 | ['light', 'sepia', 'dark'].forEach((t) => {
15 | if (allThemes[t]) {
16 | themes[t] = allThemes[t];
17 | delete allThemes[t];
18 | }
19 | });
20 | // add the rest of the themes
21 | Object.keys(allThemes).sort().forEach(t => (themes[t] = allThemes[t]));
22 |
23 | export { themes };
24 |
25 | export function applyTheme(theme) {
26 | if (!theme || !themes[theme]) {
27 | return;
28 | }
29 | Object.entries(themes[theme] || themes[gameDefaultTheme]).forEach(([prop, value]) => {
30 | document.documentElement.style.setProperty(`--${prop}`, value);
31 | });
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/src/utils/array-shuffle.js:
--------------------------------------------------------------------------------
1 | export default function arrayShuffle(array) {
2 | if (!Array.isArray(array)) {
3 | throw new TypeError(`Expected an array, got ${typeof array}`);
4 | }
5 |
6 | if (array.length === 0) {
7 | return [];
8 | }
9 |
10 | const shuffled = JSON.parse(JSON.stringify(array));
11 |
12 | for (let index = shuffled.length - 1; index > 0; index--) {
13 | const newIndex = Math.floor(Math.random() * (index + 1));
14 | [shuffled[index], shuffled[newIndex]] = [shuffled[newIndex], shuffled[index]];
15 | }
16 |
17 | return shuffled;
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/get-tag-attributes.js:
--------------------------------------------------------------------------------
1 | export default function getTagAttributes(tag) {
2 | const attrs = tag.match(/(\w+)=["']?((?:.?(?!["']?\s+(?:\S+)=|\s*\/?[>"']))+.)["']?/g);
3 | const attributes = {};
4 | attrs && attrs.forEach((item) => {
5 | const [, name, value] = item.match(/(.+)=(.+)/);
6 | try {
7 | attributes[name] = JSON.parse(value);
8 | } catch(e) {
9 | attributes[name] = value;
10 | }
11 | });
12 | return attributes;
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/mute-when-inactive.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | export default function muteWhenInactive(atrament) {
3 | document.addEventListener('visibilitychange', () => {
4 | if (document.visibilityState === 'visible') {
5 | atrament.interfaces.sound.mute(atrament.settings.get('mute'));
6 | } else {
7 | atrament.interfaces.sound.mute(true);
8 | }
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/neutralino-out-of-bounds-fix.js:
--------------------------------------------------------------------------------
1 | async function neutralinoOutOfBoundsFix() {
2 | const maxBounds = 10000;
3 | const xOutOfBounds = window.screenX < -maxBounds || window.screenX > window.screen.availWidth + maxBounds;
4 | const yOutOfBounds = window.screenY < -maxBounds || window.screenY > window.screen.availHeight + maxBounds;
5 | if (xOutOfBounds || yOutOfBounds) {
6 | console.warn("fixing unmaximize bug", window.screenX, window.screenY);
7 | await window.Neutralino.window.center();
8 | }
9 | }
10 |
11 | if (window.Neutralino) {
12 | addEventListener("resize", neutralinoOutOfBoundsFix);
13 | addEventListener('DOMContentLoaded', neutralinoOutOfBoundsFix);
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/page-background.js:
--------------------------------------------------------------------------------
1 | export const setPageBackground = (background, getAssetPath) => {
2 | if (background) {
3 | document.body.style.backgroundImage = `url(${getAssetPath(background)})`
4 | document.body.style.backgroundSize = 'cover';
5 | document.body.style.backgroundPosition = 'center';
6 | } else {
7 | document.body.style.backgroundImage = 'none';
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/utils/preload-images.js:
--------------------------------------------------------------------------------
1 | export default function preloadImages(getAssetPath, imageList) {
2 | const imagePreloads = [];
3 | for (let img of imageList) {
4 | imagePreloads.push(new Promise((resolve) => {
5 | const imgToPreload = new Image();
6 | imgToPreload.onload = resolve;
7 | imgToPreload.onerror = resolve;
8 | imgToPreload.src = getAssetPath(img);
9 | }));
10 | }
11 | return Promise.allSettled(imagePreloads);
12 | }
13 |
--------------------------------------------------------------------------------
/tools/ink-compile.cjs:
--------------------------------------------------------------------------------
1 | /*
2 | Compiles Ink to JSON, using Inklecate compiler for host OS.
3 | To enforce using JS compiler from InkJS, add this line to attrament.config.json:
4 | "inkjscompiler": true
5 | */
6 |
7 | const { spawn, spawnSync } = require('node:child_process');
8 | const os = require('node:os');
9 | const fs = require('node:fs');
10 |
11 | const cfg = require('../atrament.config.json');
12 |
13 | if (!cfg.game.source) {
14 | process.exit(0);
15 | }
16 |
17 | const format = process.argv[2] || 'json';
18 |
19 | const inputFile = `root/${cfg.game.path}/${cfg.game.source}`;
20 | const outputFile = `root/${cfg.game.path}/${cfg.game.source}.${format}`;
21 |
22 | function removeOldCompiledScript(name) {
23 | if (fs.existsSync(name)) {
24 | fs.unlinkSync(name);
25 | }
26 | }
27 |
28 | function checkInstallInklecate(compiler) {
29 | if (!fs.existsSync(compiler)) {
30 | // let's try to install inklecate
31 | console.log("Inklecate compiler is not found, installing from GitHub...");
32 | const res = spawnSync('node', ['tools/install-inklecate.cjs']);
33 | [ res.stdout.toString(), res.stderr.toString() ].forEach(
34 | (item) => item && console.log(item)
35 | );
36 | }
37 | if (!fs.existsSync(compiler)) {
38 | console.error("Failed to install Inklecate");
39 | process.exit(1);
40 | }
41 | }
42 |
43 | const runInklecate = (cmd, ...args) => {
44 | console.log('>>>', cmd, args.join(' '));
45 |
46 | const inkCompilerProcess = spawn(cmd, args);
47 |
48 | inkCompilerProcess.stdout.on('data', (data) => {
49 | console.log(data.toString());
50 | });
51 |
52 | inkCompilerProcess.stderr.on('data', (data) => {
53 | console.error(data.toString());
54 | });
55 |
56 | inkCompilerProcess.on('close', () => {
57 | if (format === 'js') {
58 | // convert json output to JS
59 | const content = fs.readFileSync(outputFile, { encoding: 'utf8', flag: 'r' });
60 | const output = `var storyContent = ${content};`;
61 | fs.writeFileSync(outputFile, output);
62 | }
63 | process.exit(inkCompilerProcess.exitCode)
64 | });
65 | }
66 |
67 | const inklecateRun = {
68 | js: ['node', 'node_modules/inkjs/dist/inkjs-compiler.js', inputFile, '-o', outputFile],
69 | win32: ['tools/inklecate/inklecate.exe', '-o', outputFile, inputFile],
70 | linux: ['tools/inklecate/inklecate', '-o', outputFile, inputFile],
71 | darwin: ['tools/inklecate/inklecate', '-o', outputFile, inputFile]
72 | }
73 |
74 | let env = 'js';
75 | if (!cfg.inkjscompiler) {
76 | env = os.platform();
77 | if (!['win32', 'linux', 'darwin'].includes(env)) {
78 | console.log(`Unsupported OS (${env}), falling back to JS compiler.`);
79 | env = 'js';
80 | } else {
81 | // check if compiler is installed
82 | checkInstallInklecate(inklecateRun[env][0]);
83 | // ensure Ink compiler is executable
84 | fs.chmodSync(inklecateRun[env][0], '755');
85 | }
86 | }
87 |
88 | // clean old compiles
89 | removeOldCompiledScript(`${inputFile}.js`);
90 | removeOldCompiledScript(`${inputFile}.json`);
91 | // run compiler
92 | runInklecate(...inklecateRun[env]);
93 |
--------------------------------------------------------------------------------
/tools/install-inklecate.cjs:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs');
2 | const os = require('node:os');
3 |
4 | const user = 'inkle';
5 | const repo = 'ink';
6 | const outputdir = `./tools/inklecate`;
7 | const leaveZipped = false;
8 | const disableLogging = false;
9 |
10 | const platforms = {
11 | win32: 'windows',
12 | linux: 'linux',
13 | darwin: 'mac'
14 | }
15 |
16 | async function downloadInklecate(platform) {
17 | const { downloadRelease } = await import('@terascope/fetch-github-release');
18 | const filterRelease = (release) => (release.prerelease === false);
19 | const filterAsset = (asset) => asset.name.includes(platform);
20 | await fs.promises.mkdir(outputdir, { recursive: true });
21 | await downloadRelease(user, repo, outputdir, filterRelease, filterAsset, leaveZipped, disableLogging);
22 | if (platform !== 'windows') {
23 | await fs.promises.chmod(`${outputdir}/inklecate`, '755');
24 | }
25 | }
26 |
27 | async function main() {
28 | const env = os.platform();
29 | const platform = platforms[env];
30 | if (platform) {
31 | try {
32 | await downloadInklecate(platform);
33 | } catch (e) {
34 | console.error(e);
35 | }
36 | } else {
37 | console.warn(`Inklecate is not available for platform: ${env}. Atrament UI will use JS compiler instead.`);
38 | }
39 | }
40 |
41 | main();
42 |
--------------------------------------------------------------------------------
/tools/neutralino-postbuild.mjs:
--------------------------------------------------------------------------------
1 | import { zip } from 'zip-a-folder';
2 | import { readdir } from 'node:fs/promises';
3 |
4 | async function main() {
5 | const files = await readdir('.');
6 | const appName = files[0];
7 | await zip('**/*-linux_*, **/*.neu', `${appName}-linux.zip`);
8 | await zip('**/*-mac_*, **/*.neu', `${appName}-mac.zip`);
9 | await zip('**/*-win_*, **/*.neu', `${appName}-windows.zip`);
10 | }
11 |
12 | main();
13 | console.log('>>> Standalone build complete.');
14 |
--------------------------------------------------------------------------------
/tools/neutralino-prepare.cjs:
--------------------------------------------------------------------------------
1 | const { spawn } = require('node:child_process');
2 | const fs = require('node:fs');
3 | const copy = require('recursive-copy');
4 | const cfg = require('../atrament.config.json');
5 |
6 | const TOOLS_DIR = 'tools/neutralino';
7 | const BUILD_DIR = 'build/.tmp_neutralino';
8 |
9 | const neutralinoConfig = {
10 | $schema: "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json",
11 | applicationId: cfg.short_name || "atrament.neutralino.ui",
12 | version: cfg.version || "1.0.0",
13 | defaultMode: "window",
14 | port: 0,
15 | documentRoot: "/resources/",
16 | url: "/",
17 | enableServer: true,
18 | enableNativeAPI: true,
19 | tokenSecurity: "one-time",
20 | logging: {
21 | enabled: false
22 | },
23 | nativeAllowList: [
24 | "app.*",
25 | "storage.*",
26 | "window.*"
27 | ],
28 | modes: {
29 | window: {
30 | title: cfg.name || "Atrament UI",
31 | width: 600,
32 | height: 800,
33 | minWidth: 400,
34 | minHeight: 600,
35 | center: true,
36 | fullScreen: false,
37 | alwaysOnTop: false,
38 | icon: "/resources/pwa-512x512.png",
39 | enableInspector: false,
40 | borderless: false,
41 | maximize: true,
42 | hidden: false,
43 | resizable: true,
44 | exitProcessOnClose: true
45 | },
46 | browser: {
47 | nativeBlockList: [
48 | "filesystem.*"
49 | ]
50 | }
51 | },
52 | cli: {
53 | binaryName: cfg.name ? cfg.name.replace(/ /g, '_') : "Atrament_UI",
54 | resourcesPath: "/resources/",
55 | extensionsPath: "/extensions/",
56 | clientLibrary: "/resources/neutralino.js"
57 | }
58 | };
59 |
60 |
61 |
62 | function copyNeutralinoFiles() {
63 | console.log('>>> Copy Neutralino binaries to build dir');
64 | copy(TOOLS_DIR, BUILD_DIR, { overwrite: true })
65 | .then((results) => {
66 | console.info(`Copied ${results.length} files`);
67 | }).catch((error) => {
68 | console.error(`Copy failed: ${error}`);
69 | });
70 | }
71 |
72 |
73 | // 1. Create config file
74 |
75 | if (!fs.existsSync(TOOLS_DIR)) {
76 | console.log(`>>> Create tools directory ${TOOLS_DIR}`);
77 | fs.mkdirSync(TOOLS_DIR);
78 | }
79 | console.log('>>> Generate neutralino.config.json');
80 | fs.writeFileSync(
81 | `${TOOLS_DIR}/neutralino.config.json`,
82 | JSON.stringify(neutralinoConfig, undefined, " ")
83 | );
84 |
85 | // 2. Download Neutralino binaries, if needed
86 |
87 | if (!fs.existsSync(`${TOOLS_DIR}/bin`)) {
88 | console.log('>>> Download Neutralino binaries');
89 | const neu = spawn('npx', ['neu', 'update'], { cwd: TOOLS_DIR, shell: true });
90 |
91 | neu.stdout.on('data', (data) => {
92 | console.log(data.toString());
93 | });
94 |
95 | neu.stderr.on('data', (data) => {
96 | console.error(`ERROR: ${data.toString()}`);
97 | });
98 |
99 | neu.on('close', (code) => {
100 | if (!fs.existsSync(`${TOOLS_DIR}/bin`)) {
101 | console.error("Failed to install Neutralino");
102 | process.exit(1);
103 | } else {
104 | copyNeutralinoFiles();
105 | }
106 | });
107 | } else {
108 | copyNeutralinoFiles();
109 | }
110 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import preact from '@preact/preset-vite';
3 | import { VitePWA } from 'vite-plugin-pwa';
4 | import { createHtmlPlugin } from 'vite-plugin-html';
5 | import { viteSingleFile } from 'vite-plugin-singlefile';
6 | import zipPack from "vite-plugin-zip-pack";
7 | import CleanBuild from 'vite-plugin-clean-build';
8 |
9 | import { compileInk, watchInkFiles } from './vite/ink-compiler-plugin';
10 | import { removeInkFilesFromBuild } from './vite/remove-ink-files-plugin';
11 | import getPWAConfig from './vite/pwa-config';
12 |
13 | import atramentCfg from './atrament.config.json';
14 |
15 | export default defineConfig(({ mode }) => {
16 | const inkCompileFormat = atramentCfg.game.format || (mode === 'singlefile' ? 'js' : 'json');
17 |
18 | const neutralinoTemplate = mode === 'standalone'
19 | ? ''
20 | : '';
21 |
22 | const plugins = [
23 | preact(),
24 | createHtmlPlugin({
25 | inject: {
26 | data: {
27 | title: atramentCfg.name,
28 | description: atramentCfg.description,
29 | neutralino: neutralinoTemplate
30 | },
31 | },
32 | }),
33 | watchInkFiles(inkCompileFormat),
34 | compileInk(inkCompileFormat),
35 | removeInkFilesFromBuild(),
36 | ]
37 |
38 | let buildDir = 'build/web';
39 |
40 | if (mode === 'singlefile') {
41 | plugins.push(viteSingleFile());
42 | buildDir = 'build/singlefile';
43 | } else if (mode === 'standalone') {
44 | plugins.push(VitePWA(getPWAConfig(atramentCfg)));
45 | buildDir = 'build/.tmp_neutralino/resources';
46 | } else if (mode === 'production') {
47 | plugins.push(VitePWA(getPWAConfig(atramentCfg)));
48 | if (atramentCfg.game.zip) {
49 | const gameDir = `${buildDir}/${atramentCfg.game.path}`;
50 | plugins.push(zipPack({
51 | inDir: gameDir,
52 | outDir: 'build',
53 | outFileName: atramentCfg.game.zip
54 | }));
55 | // delete game folder after zipping
56 | plugins.push(CleanBuild({
57 | outputDir: buildDir,
58 | patterns: [
59 | atramentCfg.game.path,
60 | ]
61 | }));
62 | }
63 | }
64 |
65 | return {
66 | plugins,
67 | define: {
68 | __INK_SCRIPT__: JSON.stringify(`${atramentCfg.game.source}.${inkCompileFormat}`),
69 | __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
70 | __EMBED_FONTS__: process.argv.includes('--embed-fonts')
71 | },
72 | resolve: {
73 | alias: [
74 | { find: 'src', replacement: "/src" },
75 | { find: 'inkjs', replacement: '/node_modules/inkjs/dist/ink.mjs' }
76 | ],
77 | },
78 | server: {
79 | port: 8900
80 | },
81 | build: {
82 | outDir: buildDir
83 | },
84 | publicDir: 'root',
85 | base: ''
86 | };
87 | });
88 |
--------------------------------------------------------------------------------
/vite/ink-compiler-plugin.js:
--------------------------------------------------------------------------------
1 | import { spawnSync } from 'child_process';
2 |
3 | function renderError(server, error) {
4 | if (error) {
5 | server.ws.send({
6 | type: 'error',
7 | err: {
8 | message: error
9 | }
10 | });
11 | }
12 | }
13 |
14 |
15 | function runCompiler(format = 'json') {
16 | const res = spawnSync('node', ['tools/ink-compile.cjs', format]);
17 | [ res.stdout.toString(), res.stderr.toString() ].forEach(
18 | (item) => item && console.log(item)
19 | );
20 | return res;
21 | }
22 |
23 | export function compileInk(format) {
24 | let inkCompilerError;
25 | return {
26 | name: 'compile-ink',
27 | configureServer(server) {
28 | renderError(server, inkCompilerError);
29 | },
30 | configResolved({ mode }) {
31 | const res = runCompiler(format);
32 | if (res.status !== 0) {
33 | inkCompilerError = res.stdout.toString();
34 | if (mode !== 'development') {
35 | throw new Error(inkCompilerError);
36 | }
37 | }
38 | return res;
39 | },
40 | }
41 | }
42 |
43 | export function watchInkFiles(format) {
44 | return {
45 | name: 'watch-ink-files-hmr',
46 | enforce: 'post',
47 | handleHotUpdate({ file, server }) {
48 | if (file.endsWith('.ink')) {
49 | const res = runCompiler(format);
50 | if (res.status === 0) {
51 | server.hot.send({
52 | type: 'full-reload',
53 | path: '*'
54 | });
55 | } else {
56 | renderError(server, res.stdout.toString())
57 | }
58 | }
59 | },
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/vite/pwa-assets.config.js:
--------------------------------------------------------------------------------
1 | import {
2 | defineConfig,
3 | minimal2023Preset as preset
4 | } from '@vite-pwa/assets-generator/config';
5 |
6 | export default defineConfig({
7 | headLinkOptions: {
8 | preset: '2023'
9 | },
10 | preset,
11 | images: ['root/logo.png']
12 | });
13 |
--------------------------------------------------------------------------------
/vite/pwa-config.js:
--------------------------------------------------------------------------------
1 | export default function getPWAConfig(atramentCfg) {
2 | let pwaConfig = {
3 | registerType: 'autoUpdate',
4 | includeAssets: ['**/!(*.ink)'],
5 | workbox: {
6 | globPatterns: ['**/*.{js,css,html,woff2}'],
7 | },
8 | manifest: {
9 | name: atramentCfg.name,
10 | short_name: atramentCfg.short_name,
11 | description: atramentCfg.description,
12 | start_url: "./",
13 | display: "standalone",
14 | orientation: "portrait",
15 | background_color: "#fff",
16 | theme_color: "#673ab8",
17 | icons: [
18 | {
19 | src: "./pwa-192x192.png",
20 | type: "image/png",
21 | sizes: "192x192"
22 | },
23 | {
24 | src: "./pwa-512x512.png",
25 | type: "image/png",
26 | sizes: "512x512"
27 | }
28 | ]
29 | },
30 | pwaAssets: {
31 | config: 'vite/pwa-assets.config.js'
32 | }
33 | };
34 |
35 | if (atramentCfg.game.zip) {
36 | // game folder will be removed, so don't include these files into service worker
37 | pwaConfig.includeAssets = [ `!./${atramentCfg.game.path}/**` ];
38 | }
39 |
40 | return pwaConfig;
41 | }
42 |
--------------------------------------------------------------------------------
/vite/remove-ink-files-plugin.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import fs from 'fs';
3 |
4 | function findFiles(startPath, filter, callback) {
5 | if (!fs.existsSync(startPath)) {
6 | console.log("no dir ", startPath);
7 | return;
8 | }
9 | const files = fs.readdirSync(startPath);
10 | files.forEach((file) => {
11 | const filename = path.join(startPath, file);
12 | const stat = fs.lstatSync(filename);
13 | if (stat.isDirectory()) {
14 | findFiles(filename, filter, callback); //recurse
15 | } else if (filter.test(filename)) callback(filename);
16 | });
17 | }
18 |
19 | export function removeInkFilesFromBuild() {
20 | return {
21 | name: 'remove-files-from-build',
22 | enforce: 'post',
23 | apply: 'build',
24 | writeBundle(options) {
25 | findFiles(options.dir, /\.ink$/, (item) => {
26 | console.log(`Removing Ink file from bundle: ${item}`);
27 | fs.unlinkSync(item);
28 | });
29 | },
30 | };
31 | }
--------------------------------------------------------------------------------