Muchos stuff

This commit is contained in:
DrMint 2023-09-11 21:14:43 +02:00
parent 1f229b4ce9
commit 421dc1e614
64 changed files with 1478 additions and 371 deletions

View File

@ -1,8 +1,4 @@
{
"editor.rulers": [100],
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": true
},
"editor.tabSize": 2
}

View File

@ -1,54 +1,7 @@
# Astro Starter Kit: Basics
# Accords Library
```
npm create astro@latest -- --template basics
```
## Stack
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```
/
├── public/
│ └── favicon.svg
├── src/
│ ├── components/
│ │ └── Card.astro
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
- Web framework: [Astro 3.X](https://astro.build/)
- JS toolkit and runtime: [Bun 1.X](https://bun.sh/)
- CMS: [Payload](https://payloadcms.com/)

View File

@ -1,6 +1,6 @@
import node from "@astrojs/node";
import icon from "astro-icon";
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
// https://astro.build/config
export default defineConfig({
@ -18,8 +18,9 @@ export default defineConfig({
},
}),
],
server: {
port: import.meta.env.ASTRO_PORT,
host: import.meta.env.ASTRO_HOST,
port: 12499,
host: true,
},
});

BIN
bun.lockb

Binary file not shown.

View File

@ -4,7 +4,7 @@
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"build": "astro check && tsc --noEmit && astro build",
"start": "astro preview",
"script:download-payload-sdk": "bun run scripts/download-payload-sdk.ts",
"precommit": "npm run script:download-payload-sdk",
@ -12,15 +12,22 @@
},
"dependencies": {
"@astrojs/node": "^6.0.0",
"@hotwired/turbo": "^7.3.0",
"accept-language": "^3.0.18",
"astro": "^3.0.3",
"astro-icon": "next",
"hono": "^3.5.7",
"htmx.org": "^1.9.5",
"js-cookie": "^3.0.5",
"node-cache": "^5.1.2",
"sanitize.css": "^13.0.0"
"sanitize.css": "^13.0.0",
"tippy.js": "^6.3.7"
},
"devDependencies": {
"@astrojs/check": "^0.2.0",
"@iconify-json/material-symbols": "^1.1.57",
"bun-types": "^0.8.1"
"@types/js-cookie": "^3.0.3",
"bun-types": "^0.8.1",
"typescript": "^5.2.2"
}
}

28
public/css/global.css Normal file
View File

@ -0,0 +1,28 @@
/*
original light mode
color: #fff1e0;
color: #ffedd8;
color: #f0d1b3;
color: #c0845e;
color: #9c6644;
color: #1b1811; */
/*
original dark mode
color: #191914;
color: #26221e;
color: #2c2803;
color: #392d22;
color: #c0845e;
color: #ebeae7; */
/* new version using https://leonardocolor.io/theme.html */
:where(button) {
background-color: inherit;
color: inherit;
border: initial;
padding: initial;
margin: initial;
cursor: pointer;
}

1
public/css/sanitize.min.css vendored Normal file
View File

@ -0,0 +1 @@
*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:root){cursor:default;line-height:1.5;overflow-wrap:break-word;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%}:where(body){margin:0}:where(h1){font-size:2em;margin:0.67em 0}:where(dl,ol,ul):where(dl,ol,ul){margin:0}:where(hr){color:inherit;height:0}:where(nav):where(ol,ul){list-style-type:none;padding:0}:where(nav li)::before{content:"\200B";float:left}:where(pre){font-family:monospace, monospace;font-size:1em;overflow:auto}:where(abbr[title]){text-decoration:underline;text-decoration:underline dotted}:where(b,strong){font-weight:bolder}:where(code,kbd,samp){font-family:monospace, monospace;font-size:1em}:where(small){font-size:80%}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}:where(iframe){border-style:none}:where(svg:not([fill])){fill:currentColor}:where(table){border-collapse:collapse;border-color:inherit;text-indent:0}:where(button,input,select){margin:0}:where(button,[type="button" i],[type="reset" i],[type="submit" i]){-webkit-appearance:button}:where(fieldset){border:1px solid #a0a0a0}:where(progress){vertical-align:baseline}:where(textarea){margin:0;resize:vertical}:where([type="search" i]){-webkit-appearance:textfield;outline-offset:-2px}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-input-placeholder{color:inherit;opacity:0.54}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}:where(dialog){background-color:white;border:solid;color:black;height:-moz-fit-content;height:fit-content;left:0;margin:auto;padding:1em;position:absolute;right:0;width:-moz-fit-content;width:fit-content}:where(dialog:not([open])){display:none}:where(details > summary:first-of-type){display:list-item}:where([aria-busy="true" i]){cursor:progress}:where([aria-controls]){cursor:pointer}:where([aria-disabled="true" i],[disabled]){cursor:not-allowed}:where([aria-hidden="false" i][hidden]){display:initial}:where([aria-hidden="false" i][hidden]:not(:focus)){clip:rect(0, 0, 0, 0);position:absolute}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,20 @@
---
import { Icon } from "astro-icon/components";
import { getLocalizedUrl } from "utils/urls";
interface Props {
href: string;
isActive?: boolean;
text?: string;
icon?: string;
}
const { locale } = Astro.params;
const { href, text, icon, isActive } = Astro.props;
---
<a href={getLocalizedUrl(href, locale)} class:list={{ active: isActive }}>
{text && text}
{icon && <Icon name={icon} />}
</a>
<style></style>

View File

@ -1,61 +0,0 @@
---
interface Props {
title: string;
body: string;
href: string;
}
const { href, title, body } = Astro.props;
---
<li class="link-card">
<a href={href}>
<h2>
{title}
<span>&rarr;</span>
</h2>
<p>
{body}
</p>
</a>
</li>
<style>
.link-card {
list-style: none;
display: flex;
padding: 1px;
background-color: #23262d;
background-image: none;
background-size: 400%;
border-radius: 7px;
background-position: 100%;
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}
.link-card > a {
width: 100%;
text-decoration: none;
line-height: 1.4;
padding: calc(1.5rem - 1px);
border-radius: 8px;
color: white;
background-color: #23262d;
opacity: 0.8;
}
h2 {
margin: 0;
font-size: 1.25rem;
transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
p {
margin-top: 0.5rem;
margin-bottom: 0;
}
.link-card:is(:hover, :focus-within) {
background-position: 0;
background-image: var(--accent-gradient);
}
.link-card:is(:hover, :focus-within) h2 {
color: rgb(var(--accent-light));
}
</style>

View File

@ -1,40 +0,0 @@
<svg
class="astro-a"
width="495"
height="623"
viewBox="0 0 495 623"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M167.19 364.254C83.4786 364.254 0 404.819 0 404.819C0 404.819 141.781 19.4876 142.087 18.7291C146.434 7.33701 153.027 0 162.289 0H332.441C341.703 0 348.574 7.33701 352.643 18.7291C352.92 19.5022 494.716 404.819 494.716 404.819C494.716 404.819 426.67 364.254 327.525 364.254L264.41 169.408C262.047 159.985 255.147 153.581 247.358 153.581C239.569 153.581 232.669 159.985 230.306 169.408L167.19 364.254ZM160.869 530.172C160.877 530.18 160.885 530.187 160.894 530.195L160.867 530.181C160.868 530.178 160.868 530.175 160.869 530.172ZM136.218 411.348C124.476 450.467 132.698 504.458 160.869 530.172C160.997 529.696 161.125 529.242 161.248 528.804C161.502 527.907 161.737 527.073 161.917 526.233C165.446 509.895 178.754 499.52 195.577 500.01C211.969 500.487 220.67 508.765 223.202 527.254C224.141 534.12 224.23 541.131 224.319 548.105C224.328 548.834 224.337 549.563 224.347 550.291C224.563 566.098 228.657 580.707 237.264 593.914C245.413 606.426 256.108 615.943 270.749 622.478C270.593 621.952 270.463 621.508 270.35 621.126C270.045 620.086 269.872 619.499 269.685 618.911C258.909 585.935 266.668 563.266 295.344 543.933C298.254 541.971 301.187 540.041 304.12 538.112C310.591 533.854 317.059 529.599 323.279 525.007C345.88 508.329 360.09 486.327 363.431 457.844C364.805 446.148 363.781 434.657 359.848 423.275C358.176 424.287 356.587 425.295 355.042 426.275C351.744 428.366 348.647 430.33 345.382 431.934C303.466 452.507 259.152 455.053 214.03 448.245C184.802 443.834 156.584 436.019 136.218 411.348Z"
fill="url(#paint0_linear_1805_24383)"></path>
<defs>
<linearGradient
id="paint0_linear_1805_24383"
x1="247.358"
y1="0"
x2="247.358"
y2="622.479"
gradientUnits="userSpaceOnUse"
>
<stop stop-opacity="0.9"></stop>
<stop offset="1" stop-opacity="0.2"></stop>
</linearGradient>
</defs>
</svg>
<style>
.astro-a {
position: absolute;
top: -32px;
left: 50%;
transform: translatex(-50%);
width: 220px;
height: auto;
z-index: -1;
}
</style>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,262 @@
---
import { Icon } from "astro-icon/components";
import { getLocalizedUrl } from "utils/urls";
import { CookieNames } from "utils/cookies";
import NavOption from "./NavOption.astro";
const { locale = "en" } = Astro.params;
const isReduced =
Astro.cookies.get(CookieNames.MENU_PANEL_REDUCED)?.boolean() ?? false;
const themeColors =
Astro.cookies.get(CookieNames.THEME_COLOR)?.value ?? "theme-color-light";
---
<div id="component" class:list={{ reduced: isReduced }}>
<a
id="accords-logo"
data-turbo-confirm="Do you want to leave this page?"
href={getLocalizedUrl("/", locale)}
>
<Icon name="accords" />
</a>
<p id="title">Accords Library</p>
<button id="reduce-toggle">
<Icon name="material-symbols:chevron-left-rounded" width={24} height={24} />
</button>
<button
class={themeColors === "theme-color-dark" ? "dark" : "light"}
id="theme-toggle"
>
<Icon class="when-light" name="material-symbols:light-mode" />
<Icon class="when-dark" name="material-symbols:dark-mode" />
</button>
<NavOption
href="/library"
icon="material-symbols:auto-stories"
title="Library"
subtitle="Browse all physical and digital media"
/>
<NavOption
href="/contents"
icon="material-symbols:workspaces"
title="Content"
subtitle="Explore all content and filter by type or category"
/>
<NavOption
href="/wiki"
icon="material-symbols:travel-explore"
title="Wiki"
subtitle="An encyclopedia for everything related to DrakeNieR"
/>
<NavOption
href="/chronicles"
icon="material-symbols:schedule"
title="Chronicles"
subtitle="Experience all events and content in chronological order"
/>
<NavOption href="/news" icon="material-symbols:newspaper" title="News" />
<NavOption
href="https://gallery.accords-library.com/posts/"
icon="material-symbols:perm-media"
title="Gallery"
/>
<NavOption href="/archives" icon="material-symbols:save" title="Archives" />
<NavOption href="/about-us" icon="material-symbols:info" title="About us" />
<p>
This websites content is made available under <a
href="https://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA</a
> unless otherwise noted.
</p>
<div id="common-creative">
<Icon name="creative-commons-brands" />
<Icon name="creative-commons-by-brands" />
<Icon name="creative-commons-sa-brands" />
</div>
<p>
Accords Library is not affiliated with or endorsed by SQUARE ENIX CO. LTD.
All game assets and promotional materials belongs to © SQUARE ENIX CO. LTD.
</p>
<div id="social-links">
<Icon name="github-brands" />
<Icon name="x-brands" />
<Icon name="discord-brands" />
</div>
</div>
<script>
import { CookieNames } from "utils/cookies";
import { observableWithPersistence } from "utils/micro-observables";
import { Elementos } from "utils/Elementos";
import { onLoad } from "utils/turbo";
onLoad(() => {
const component = new Elementos("#component");
const reduceToggleButton = new Elementos("#reduce-toggle");
const isReduced = observableWithPersistence(
CookieNames.MENU_PANEL_REDUCED,
false
);
reduceToggleButton.onClick(() => {
isReduced.update((oldValue) => !oldValue);
Turbo.cache.clear();
});
component.setClass("reduced", isReduced);
// theme-toggle
const body = new Elementos("body");
const themeToggleButton = new Elementos("#theme-toggle");
const themeColor = observableWithPersistence(
CookieNames.THEME_COLOR,
"theme-color-light"
);
const isDarkMode = themeColor.select(
(value) => value === "theme-color-dark"
);
themeToggleButton.onClick(() => {
themeColor.update((oldValue) =>
oldValue === "theme-color-light"
? "theme-color-dark"
: "theme-color-light"
);
Turbo.cache.clear();
});
themeToggleButton.setClass(
{ ifFalse: "light", ifTrue: "dark" },
isDarkMode
);
body.setClass(
{ ifFalse: "theme-color-light", ifTrue: "theme-color-dark" },
isDarkMode
);
});
</script>
<style>
#component {
--reduced-transition-duration: 5s;
display: grid;
padding: 2rem;
gap: 1rem;
container-type: inline-size;
@media (width >= 60rem) {
transition:
var(--reduced-transition-duration) width,
var(--reduced-transition-duration) padding;
position: relative;
width: 20rem;
flex-shrink: 0;
border-right: var(--border-style);
& > #title {
font-family: var(--font-headers);
font-size: 1.875rem;
margin: 0;
}
& > p {
transition:
var(--reduced-transition-duration) font-size,
var(--reduced-transition-duration) opacity;
}
& > #common-creative,
& > #social-links {
transition:
var(--reduced-transition-duration) transform,
var(--reduced-transition-duration) opacity;
}
&.reduced {
width: 6rem;
padding: 2rem 1rem;
& > p,
& > #title {
font-size: 0;
opacity: 0;
}
& > #common-creative,
& > #social-links {
transform: scale(0);
opacity: 0;
}
}
}
& > p {
text-align: center;
}
& > #accords-logo {
place-self: center;
width: max(50%, 3rem);
aspect-ratio: 1/1;
color: var(--color-base-1000);
transition: 0.1s color;
&:hover {
color: var(--color-base-600);
}
& > svg {
width: 100%;
height: 100%;
}
}
& > #theme-toggle.dark > .when-light,
& > #theme-toggle.light > .when-dark {
display: none;
}
& > #common-creative {
display: grid;
grid-auto-flow: column;
place-content: center;
gap: 0.25rem;
& > svg {
width: 24px;
height: 24px;
}
}
& > #social-links {
display: grid;
grid-auto-flow: column;
place-items: center;
& > svg {
width: 40px;
height: 40px;
}
}
}
</style>

View File

@ -0,0 +1,94 @@
---
import { Icon } from "astro-icon/components";
import { getLocalizedUrl } from "utils/urls";
interface Props {
href: string;
icon?: string;
title: string | null | undefined;
subtitle?: string | null | undefined;
}
const { locale } = Astro.params;
const { href, icon, title, subtitle } = Astro.props;
const currentPath = new URL(Astro.request.url).pathname.slice(
`/${locale}`.length
);
const isActive = currentPath.startsWith(href);
---
<a
id="component"
href={getLocalizedUrl(href, locale)}
class:list={{ active: isActive }}
>
{icon && <Icon name={icon} />}
<div>
<p id="title">
{title}
</p>
<p>
{subtitle}
</p>
</div>
</a>
<style>
#component {
@container (max-width: 15rem) {
width: 3.5rem;
& > div {
& > #title,
& > p {
display: none;
}
}
}
padding: 1rem;
color: inherit;
text-decoration: inherit;
display: flex;
width: 100%;
gap: 1.25rem;
border-radius: 1rem;
transition:
0.15s background-color,
0.15s box-shadow;
&:hover,
&.active {
box-shadow: inset 0 1px 4px -2px var(--color-shadow);
background-color: var(--color-base-250);
&:active {
box-shadow: inset 0 2px 4px 0 var(--color-shadow);
}
}
& > svg {
width: 24px;
height: 24px;
flex-shrink: 0;
}
& > div {
display: grid;
flex-grow: 1;
& > #title {
line-height: 1.1;
font-size: 1.5rem;
font-family: var(--font-headers);
}
& > p {
margin: 0;
}
}
}
</style>

View File

@ -0,0 +1,75 @@
---
import { Icon } from "astro-icon/components";
interface Props {
title: string;
showSubPanel: boolean;
}
const { title, showSubPanel } = Astro.props;
---
<div id="component">
<button
id="toggle-menu-panel"
_="on click
toggle .opened on #menu-panel
remove .opened from #sub-panel
remove .on from #toggle-sub-panel
toggle .on on me"
>
<Icon
class="when-off"
name="material-symbols:menu-rounded"
width={24}
height={24}
/>
<Icon
class="when-on"
name="material-symbols:close-rounded"
width={24}
height={24}
/>
</button>
<p>{title}</p>
<div>
{
showSubPanel && (
<button
id="toggle-sub-panel"
_="on click
toggle .opened on #sub-panel
remove .opened from #menu-panel
remove .on from #toggle-menu-panel
toggle .on on me"
>
<Icon
class="when-off"
name="material-symbols:tune-rounded"
width={24}
height={24}
/>
<Icon
class="when-on"
name="material-symbols:close-rounded"
width={24}
height={24}
/>
</button>
)
}
</div>
</div>
<style>
#component {
border-top: var(--border-style);
display: grid;
grid-template-columns: 5rem 1fr 5rem;
place-items: center;
}
.on > .when-off,
:not(.on) > .when-on {
display: none;
}
</style>

View File

@ -1,4 +0,0 @@
import "sanitize.css";
import "src/styles/global.css";
import "src/styles/reset.css";

1
src/icons/accords.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M245.8 214.9l-33.22 17.28c-9.43-19.58-25.24-19.93-27.46-19.93-22.13 0-33.22 14.61-33.22 43.84 0 23.57 9.21 43.84 33.22 43.84 14.47 0 24.65-7.09 30.57-21.26l30.55 15.5c-6.17 11.51-25.69 38.98-65.1 38.98-22.6 0-73.96-10.32-73.96-77.05 0-58.69 43-77.06 72.63-77.06 30.72-.01 52.7 11.95 65.99 35.86zm143.1 0l-32.78 17.28c-9.5-19.77-25.72-19.93-27.9-19.93-22.14 0-33.22 14.61-33.22 43.84 0 23.55 9.23 43.84 33.22 43.84 14.45 0 24.65-7.09 30.54-21.26l31 15.5c-2.1 3.75-21.39 38.98-65.09 38.98-22.69 0-73.96-9.87-73.96-77.05 0-58.67 42.97-77.06 72.63-77.06 30.71-.01 52.58 11.95 65.56 35.86zM247.6 8.05C104.7 8.05 0 123.1 0 256c0 138.5 113.6 248 247.6 248C377.5 504 496 403.1 496 256 496 118.1 389.4 8 247.6 8zm.87 450.8c-112.5 0-203.7-93.04-203.7-202.8 0-105.4 85.43-203.3 203.7-203.3 112.5 0 202.8 89.46 202.8 203.3-.01 121.7-99.68 202.8-202.8 202.8z"/></svg>

After

Width:  |  Height:  |  Size: 925 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M314.9 194.4v101.4h-28.3v120.5h-77.1V295.9h-28.3V194.4c0-4.4 1.6-8.2 4.6-11.3 3.1-3.1 6.9-4.7 11.3-4.7H299c4.1 0 7.8 1.6 11.1 4.7 3.1 3.2 4.8 6.9 4.8 11.3zm-101.5-63.7c0-23.3 11.5-35 34.5-35s34.5 11.7 34.5 35c0 23-11.5 34.5-34.5 34.5s-34.5-11.5-34.5-34.5zM247.6 8C389.4 8 496 118.1 496 256c0 147.1-118.5 248-248.4 248C113.6 504 0 394.5 0 256 0 123.1 104.7 8 247.6 8zm.8 44.7C130.2 52.7 44.7 150.6 44.7 256c0 109.8 91.2 202.8 203.7 202.8 103.2 0 202.8-81.1 202.8-202.8.1-113.8-90.2-203.3-202.8-203.3z"/></svg>

After

Width:  |  Height:  |  Size: 579 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M247.6 8C389.4 8 496 118.1 496 256c0 147.1-118.5 248-248.4 248C113.6 504 0 394.5 0 256 0 123.1 104.7 8 247.6 8zm.8 44.7C130.2 52.7 44.7 150.6 44.7 256c0 109.8 91.2 202.8 203.7 202.8 103.2 0 202.8-81.1 202.8-202.8.1-113.8-90.2-203.3-202.8-203.3zM137.7 221c13-83.9 80.5-95.7 108.9-95.7 99.8 0 127.5 82.5 127.5 134.2 0 63.6-41 132.9-128.9 132.9-38.9 0-99.1-20-109.4-97h62.5c1.5 30.1 19.6 45.2 54.5 45.2 23.3 0 58-18.2 58-82.8 0-82.5-49.1-80.6-56.7-80.6-33.1 0-51.7 14.6-55.8 43.8h18.2l-49.2 49.2-49-49.2h19.4z"/></svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M524.5 69.84a1.5 1.5 0 00-.764-.7A485.1 485.1 0 00404.1 32.03a1.816 1.816 0 00-1.923.91 337.5 337.5 0 00-14.9 30.6 447.8 447.8 0 00-134.4 0 309.5 309.5 0 00-15.14-30.6 1.89 1.89 0 00-1.924-.91A483.7 483.7 0 00116.1 69.14a1.712 1.712 0 00-.788.676C39.07 183.7 18.19 294.7 28.43 404.4a2.016 2.016 0 00.765 1.375A487.7 487.7 0 00176 479.9a1.9 1.9 0 002.063-.676A348.2 348.2 0 00208.1 430.4a1.86 1.86 0 00-1.019-2.588 321.2 321.2 0 01-45.87-21.85 1.885 1.885 0 01-.185-3.126 251.047 251.047 0 009.109-7.137 1.819 1.819 0 011.9-.256c96.23 43.92 200.4 43.92 295.5 0a1.812 1.812 0 011.924.233 234.533 234.533 0 009.132 7.16 1.884 1.884 0 01-.162 3.126 301.4 301.4 0 01-45.89 21.83 1.875 1.875 0 00-1 2.611 391.1 391.1 0 0030.01 48.81 1.864 1.864 0 002.063.7A486 486 0 00610.7 405.7a1.882 1.882 0 00.765-1.352C623.7 277.6 590.9 167.5 524.5 69.84zm-302 267.76c-28.97 0-52.84-26.59-52.84-59.24s23.44-59.26 52.84-59.26c29.67 0 53.31 26.82 52.84 59.24-.04 31.76-23.44 59.26-52.84 59.26zm195.4 0c-28.97 0-52.84-26.59-52.84-59.24s23.34-59.26 52.84-59.26c29.67 0 53.31 26.82 52.84 59.24-.04 31.76-23.24 59.26-52.84 59.26z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"/></svg>

After

Width:  |  Height:  |  Size: 871 B

1
src/icons/x-brands.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584l-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/></svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@ -1,203 +1,140 @@
---
import { Icon } from "astro-icon/components";
import Html from "layouts/Html.astro";
import Navbar from "components/Navbar.astro";
import MenuPanel from "components/MenuPanel.astro";
interface Props {
title: string;
}
const { title } = Astro.props;
const turnSubPanelIntoMainPanel =
Astro.slots.has("subPanel") && !Astro.slots.has("mainPanel");
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
</head>
<body class="theme-color-dark">
<div id="main-panel">
<slot name="mainPanel">Main Panel</slot>
<Html title={title}>
<div
id="panels"
class:list={{ "turn-sub-into-main": turnSubPanelIntoMainPanel }}
>
<div id="menu-panel" class="texture-dots">
<MenuPanel />
</div>
<div id="backdrop"></div>
<div id="sub-panel">
<slot name="subPanel">Sub Panel</slot>
<div id="sub-panel" class="texture-dots">
<slot name="subPanel" />
</div>
<div id="menu-panel">Menu Panel</div>
<div id="navbar">
<button id="open-menu">
<Icon class="when-off" name="material-symbols:menu" width={24} height={24} />
<Icon class="when-on" name="material-symbols:close" width={24} height={24} />
</button>
{title}
<button id="open-sub">
<Icon class="when-off" name="material-symbols:tune" width={24} height={24} />
<Icon class="when-on" name="material-symbols:close" width={24} height={24} />
</button>
<div id="main-panel" class="texture-dots">
<slot name="mainPanel" />
</div>
</body>
</html>
</div>
<script>
const subPanel = document.querySelector("#sub-panel")!;
const menuPanel = document.querySelector("#menu-panel")!;
const openMenuButton = document.querySelector("#open-menu")!;
const subMenuButton = document.querySelector("#open-sub")!;
const backdrop = document.querySelector("#backdrop")!;
openMenuButton.addEventListener("click", () => {
menuPanel.classList.toggle("opened");
openMenuButton.classList.toggle("on");
subPanel.classList.remove("opened");
subMenuButton.classList.remove("on");
if (menuPanel.classList.contains("opened")) {
backdrop.classList.add("on");
} else {
backdrop.classList.remove("on");
}
});
subMenuButton.addEventListener("click", () => {
subPanel.classList.toggle("opened");
subMenuButton.classList.toggle("on");
menuPanel.classList.remove("opened");
openMenuButton.classList.remove("on");
if (subPanel.classList.contains("opened")) {
backdrop.classList.add("on");
} else {
backdrop.classList.remove("on");
}
});
backdrop.addEventListener("click", () => {
backdrop.classList.remove("on");
subPanel.classList.remove("opened");
subMenuButton.classList.remove("on");
menuPanel.classList.remove("opened");
openMenuButton.classList.remove("on");
});
</script>
<div id="navbar">
<Navbar
title={title}
showSubPanel={Astro.slots.has("subPanel") && !turnSubPanelIntoMainPanel}
/>
</div>
</Html>
<style>
body {
--border-style: 1px solid rgb(var(--theme-color-dark) / 0.5);
display: grid;
background-color: rgb(var(--theme-color-light));
color: rgb(var(--theme-color-black));
inset: 0;
position: absolute;
overflow: hidden;
position: absolute;
inset: 0;
--border-style: 1px solid var(--color-base-400);
& > div {
background-color: rgb(var(--theme-color-light));
}
& > #sub-panel,
& > #menu-panel {
border-right: var(--border-style);
}
}
@media (max-width: 60rem) {
body {
grid-template-areas: "main" "navbar";
@media (width < 60rem) {
display: grid;
grid-template-rows: 1fr 5rem;
}
& > #navbar {
display: grid;
@media (width >= 60rem) {
display: none;
}
}
& > #panels {
position: relative;
@media (width < 60rem) {
&.turn-sub-into-main > #sub-panel {
inset: 0;
z-index: 0;
transition: initial;
border: initial;
width: 100%;
}
}
@media (width >= 60rem) {
position: absolute;
inset: 0;
display: flex;
}
& > #menu-panel,
& > #sub-panel {
grid-area: main;
transition: transform 0.3s;
width: min(30rem, 90%);
@media (width < 60rem) {
right: -100%;
top: 0;
bottom: 0;
transition: 0.2s right;
z-index: 1;
border-left: var(--border-style);
&.opened {
right: 0;
}
}
@media (width >= 60rem) {
width: 20rem;
flex-shrink: 0;
border-right: var(--border-style);
}
}
& > #menu-panel {
transform: translateX(-100%);
}
background-position: top right;
flex-shrink: 0;
overflow-y: auto;
& > #menu-panel.opened {
transform: translateX(0%);
}
@media (width < 60rem) {
left: -100%;
top: 0;
bottom: 0;
transition: 0.2s left;
z-index: 1;
border-right: var(--border-style);
& > #sub-panel {
justify-self: right;
border-right: unset;
border-left: var(--border-style);
transform: translateX(100%);
}
& > #sub-panel.opened {
transform: translateX(0%);
&.opened {
left: 0;
}
}
}
& > #main-panel {
grid-area: main;
width: 100%;
}
& > #backdrop {
background-color: rgb(var(--theme-color-shade));
opacity: 0%;
transition: opacity 0.3s;
grid-area: main;
}
& > #backdrop:not(.on) {
touch-action: none;
pointer-events: none;
}
& > #backdrop.on {
opacity: 60%;
cursor: pointer;
}
& > #navbar {
grid-area: navbar;
& > div {
display: grid;
grid-template-columns: 5rem 1fr 5rem;
place-items: center;
border-top: var(--border-style);
background-color: var(--color-base-150);
justify-content: center;
& > button:not(.on) > .when-on,
& > button.on > .when-off {
&:empty {
display: none;
}
@media (width < 60rem) {
position: absolute;
width: min(30rem, 90%);
}
}
}
}
@media (max-width: 20rem) {
body {
& > #menu-panel,
& > #sub-panel {
width: 100%;
}
}
}
@media (min-width: 60rem) {
body {
grid-template-columns: 20rem 20rem 1fr;
grid-template-areas: "menu sub main";
& > #menu-panel {
grid-area: menu;
}
& > #sub-panel {
grid-area: sub;
}
& > #main-panel {
grid-area: main;
}
& > #navbar,
& > #backdrop {
display: none;
}
}
}
</style>

159
src/layouts/Html.astro Normal file
View File

@ -0,0 +1,159 @@
---
import { CookieNames } from "utils/cookies";
interface Props {
title: string;
}
const { title } = Astro.props;
const themeColors =
Astro.cookies.get(CookieNames.THEME_COLOR)?.value ?? "theme-color-light";
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
<script is:inline src="/js/turbo.es2017-umd.js" async defer></script>
<link href="/css/sanitize.min.css" rel="stylesheet" />
<link href="/css/global.css" rel="stylesheet" />
</head>
<body class={themeColors} hx-ext="head-support" hx-boost="true">
<slot />
</body>
</html>
<script>
import tippy from "tippy.js";
import "tippy.js/dist/tippy.css";
tippy("[data-tippy-content]", { allowHTML: true });
</script>
<style is:global>
.theme-color-light {
--color-base-0: #ffffff;
--color-base-50: #fffaf3;
--color-base-100: #fff4e6;
--color-base-150: #fdebd4;
--color-base-200: #f7ddc2;
--color-base-250: #efcfb0;
--color-base-300: #e5be9e;
--color-base-350: #ddb08e;
--color-base-400: #d3a07c;
--color-base-450: #ca926c;
--color-base-500: #c0835d;
--color-base-550: #b3754f;
--color-base-600: #a26a47;
--color-base-650: #905e3f;
--color-base-700: #805438;
--color-base-750: #6e4a31;
--color-base-800: #5e402b;
--color-base-850: #4d3625;
--color-base-900: #3c2d1e;
--color-base-950: #2f2419;
--color-base-1000: #1f1a13;
--color-shadow: var(--color-base-500);
--texture-dots: url(/images/paper-dots.webp);
--texture-dots-blend: multiply;
}
.theme-color-dark {
--color-base-1000: #ebeae7;
--color-base-950: #eae5e0;
--color-base-900: #e8dfd8;
--color-base-850: #e4d1c4;
--color-base-800: #e0bfaa;
--color-base-750: #dcb095;
--color-base-700: #d4a07f;
--color-base-650: #cb916c;
--color-base-600: #bf835d;
--color-base-550: #b07751;
--color-base-500: #a06b48;
--color-base-450: #8f5f40;
--color-base-400: #7d5539;
--color-base-350: #6b4a33;
--color-base-300: #5c412e;
--color-base-250: #4a3728;
--color-base-200: #3a2d22;
--color-base-150: #27231e;
--color-base-100: #1c1b16;
--color-base-50: #11110d;
--color-base-0: #000000;
--color-shadow: var(--color-base-0);
--texture-dots: url(/images/paper-dots-dark.webp);
--texture-dots-blend: overlay;
}
.texture-dots {
background-size: 10cm;
background-attachment: local;
background-image: var(--texture-dots);
background-blend-mode: var(--texture-dots-blend);
background-repeat: repeat;
}
body {
background-color: var(--color-base-150);
color: var(--color-base-1000);
}
.turbo-progress-bar {
height: 5px;
background-color: #b07751;
}
@font-face {
font-family: "Vollkorn";
src: url("/fonts/Vollkorn-Bold.woff2") format("woff2");
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: "Noto Sans";
src: url("/fonts/NotoSans-Medium.woff2") format("woff2");
font-weight: medium;
font-style: normal;
}
@font-face {
font-family: "Noto Sans";
src: url("/fonts/NotoSans-Bold.woff2") format("woff2");
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: "Angelic Agrippa";
src: url("/fonts/AngelicAgrippa-Regular.woff2") format("woff2");
font-weight: normal;
font-style: normal;
}
:root {
--font-body: "Noto Sans", sans-serif;
--font-headers: "Vollkorn", serif;
--font-angelic: "Angelic Agrippa", serif;
}
p {
font-family: var(--font-body);
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-headers);
}
</style>

View File

@ -0,0 +1,10 @@
---
import AppLayout from "layouts/AppLayout.astro";
---
<AppLayout title="Archive">
<div slot="subPanel">
Besides physical medias, we also archive digital contents such as websites,
webpages, videos, and documents.
</div>
</AppLayout>

View File

@ -1,10 +0,0 @@
---
import "src/global.ts";
import AppLayout from "src/layouts/AppLayout.astro";
---
<AppLayout title="Welcome to Astro.">
<main slot="mainPanel">
<h1>Hello from Astro</h1>
</main>
</AppLayout>

View File

@ -0,0 +1,88 @@
---
import { Icon } from "astro-icon/components";
import AppLayout from "layouts/AppLayout.astro";
---
<AppLayout title="Accords Library">
<main slot="mainPanel">
<div id="header">
<div>
<Icon name="accords" />
</div>
<h1>Accords Library</h1>
<h2 class="angelic">Discover • Analyze • Translate • Archive</h2>
</div>
<div class="formatted">
<p>
Accords Library aims at gathering and archiving all of Yoko Taros
work. Yoko Taro is a Japanese video game director and scenario writer.
He is best-known for his work on the NieR and Drakengard (Drag-on
Dragoon) franchises. To complement his games, Yoko Taro likes to publish
side materials in the form of books, novellas, artbooks, stage plays,
manga, drama CDs, and comics. Those side materials can be very difficult
to find. His work goes all the way back to 2003, and most of them are
out of print after having been released solely in Japan, sometimes in
limited quantities. Their prices on the second hand market have
skyrocketed, ranging all the way to hundreds if not thousand of dollars
for the rarest items.
</p><p>
This is where this library takes its meaning, in trying to help the
community grow by providing translators, writers, and wikis
contributors a simple way to access these records filled with stories,
artworks, and knowledge.
</p><p>
We are a small group of Yoko Taros fans that decided to join forces and
create a website and a community. Our motto is <strong
>Discover • Analyze • Translate • Archive</strong
> (D.A.T.A. for short). We started with the goal of gathering and
archiving as much side-materials/merch as possible. But since then, our
ambition grew and we decided to create a full-fledged website that will
also include news articles, lore, summaries, translations, and
transcriptions. Hopefully one day, we will be up there in the list of
notable resources for Drakengard and NieR fans.
</p>
</div>
</main>
</AppLayout>
<style>
main {
padding-left: 2.5rem;
padding-right: 2.5rem;
padding-bottom: 8rem;
padding-top: 5rem;
max-width: 42rem;
}
.angelic {
font-family: var(--font-angelic);
}
#header {
display: grid;
place-content: center;
place-items: center;
& > h1 {
font-size: 3rem;
margin-bottom: 0;
line-height: 0.9;
}
& > h2 {
margin-top: 0;
font-size: 1.125rem;
}
& > div {
aspect-ratio: 1/1;
width: 128px;
& > svg {
height: 100%;
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,13 @@
---
import AppLayout from "layouts/AppLayout.astro";
---
<AppLayout title="Library">
<main slot="mainPanel"><h1>Library</h1></main>
<div slot="subPanel">
A comprehensive list of all Yokoverses side materials (books, novellas,
artbooks, stage plays, manga, drama CDs, and comics). For each, we provide
photos, scans, and transcript of the content, information about what it is,
when and how it was released, size, initial price…
</div>
</AppLayout>

View File

@ -1,17 +0,0 @@
.theme-color-light {
--theme-color-highlight: 255 241 224;
--theme-color-light: 255 237 216;
--theme-color-mid: 240 209 179;
--theme-color-dark: 156 102 68;
--theme-color-shade: 192 132 94;
--theme-color-black: 27 24 17;
}
.theme-color-dark {
--theme-color-highlight: 44 40 37;
--theme-color-light: 38 34 30;
--theme-color-mid: 57 45 34;
--theme-color-dark: 192 132 94;
--theme-color-shade: 25 25 20;
--theme-color-black: 235 234 231;
}

View File

@ -1,8 +0,0 @@
:where(button) {
background-color: inherit;
color: inherit;
border: initial;
padding: initial;
margin: initial;
cursor: pointer;
}

View File

@ -1,4 +1,4 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference path="../../.astro/types.d.ts" />
/// <reference types="astro/client" />
interface ImportMetaEnv {

4
src/typings/html-attributes.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare namespace astroHTML.JSX {
interface HTMLAttributes {}
}

6
src/typings/turbo.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
// https://turbo.hotwired.dev/reference/drive
declare namespace Turbo {
const cache = {
clear: () => null,
};
}

26
src/utils/Elementos.ts Normal file
View File

@ -0,0 +1,26 @@
import type { Observable } from "./micro-observables";
export class Elementos {
readonly element: HTMLElement;
constructor(readonly selector: string) {
this.element = document.querySelector(selector)!;
}
onClick(listener: () => void) {
this.element.addEventListener("click", listener);
}
setClass(
className: string | { ifTrue: string; ifFalse: string },
observable: Observable<boolean>
) {
observable.subscribe((val) => {
if (typeof className === "string") {
this.element.classList.toggle(className, val);
} else {
this.element.classList.toggle(className.ifFalse, val === false);
this.element.classList.toggle(className.ifTrue, val === true);
}
});
}
}

4
src/utils/cookies.ts Normal file
View File

@ -0,0 +1,4 @@
export enum CookieNames {
MENU_PANEL_REDUCED = "menuPanelReduced",
THEME_COLOR = "themeColor",
}

View File

@ -0,0 +1,226 @@
import { batchedUpdater } from "./batchedUpdater";
import { type Plugin } from "./plugin";
import { PluginManager } from "./pluginManager";
const UNSET = Symbol();
const plugins = new PluginManager();
const capturedInputFrames: BaseObservable<any>[][] = [];
let shouldCaptureNextInput = false;
let batchedObservables: BaseObservable<any>[] = [];
let batchDepth = 0;
export type Listener<T> = (val: T, prevVal: T) => void;
export type Unsubscriber = () => void;
export type Options = { [key: string]: any };
export class BaseObservable<T> {
private _val: T;
private _prevVal: T | typeof UNSET = UNSET;
private _options: Options;
private _inputs: BaseObservable<any>[] = [];
private _outputs: BaseObservable<any>[] = [];
private _listeners: Listener<T>[] = [];
private _attachedToInputs = false;
constructor(val: T, options: Options = {}) {
this._val = val;
this._options = options;
plugins.onCreate(this, val);
}
get(): T {
const capturedInputs = capturedInputFrames[capturedInputFrames.length - 1];
if (capturedInputs && shouldCaptureNextInput) {
try {
shouldCaptureNextInput = false;
capturedInputs.push(this);
return this._get();
} finally {
shouldCaptureNextInput = true;
}
} else {
return this._get();
}
}
protected _get(): T {
const shouldEvaluate = !this._attachedToInputs || this._prevVal !== UNSET;
return shouldEvaluate ? this._evaluate() : this._val;
}
protected _evaluate(): T {
return this._val;
}
protected _set(val: T) {
if (this._val !== val) {
this._addToBatchRecursively();
this._val = val;
}
}
subscribe(listener: Listener<T>): Unsubscriber {
this._listeners.push(listener);
this._attachToInputs();
let listenerRemoved = false;
return () => {
if (!listenerRemoved) {
listenerRemoved = true;
this._listeners.splice(this._listeners.indexOf(listener), 1);
this._detachFromInputs();
}
};
}
/**
* @deprecated Use observable.subscribe() instead
*/
onChange = this.subscribe;
protected onBecomeObserved() {
// Called when the first listener subscribes to the observable or to one of its outputs
}
protected onBecomeUnobserved() {
// Called when the last listener unsubscribes from the observable and from all of its outputs
}
getInputs(): BaseObservable<any>[] {
return this._inputs;
}
getOptions<O extends Options = Options>(): O {
return this._options as O;
}
withOptions<O extends Options = Options>(options: Partial<O>): this {
this._options = { ...this._options, ...options };
return this;
}
protected static _captureInputs<T>(block: () => T): BaseObservable<any>[] {
try {
const capturedInputs: any[] = [];
capturedInputFrames.push(capturedInputs);
shouldCaptureNextInput = true;
block();
return capturedInputs;
} finally {
capturedInputFrames.pop();
shouldCaptureNextInput = false;
}
}
protected _addInput(input: BaseObservable<any>) {
this._inputs.push(input);
if (this._attachedToInputs) {
this._attachToInput(input);
}
}
protected _removeInput(input: BaseObservable<any>) {
this._inputs.splice(this._inputs.indexOf(input), 1);
if (this._attachedToInputs) {
this._detachFromInput(input);
}
}
private _shouldAttachToInputs(): boolean {
// Only attach to inputs when at least one listener is subscribed to the observable or to one of its outputs.
// This is done to avoid unused observables being references by their inputs, preventing garbage-collection.
return this._listeners.length > 0 || this._outputs.length > 0;
}
private _attachToInputs() {
if (!this._attachedToInputs && this._shouldAttachToInputs()) {
this._attachedToInputs = true;
// Since the observable was not attached to its inputs, its value may be outdated.
// Refresh it so that listeners will be called with the correct prevValue the next time an input changes.
this._val = this._evaluate();
this.onBecomeObserved();
plugins.onBecomeObserved(this);
for (const input of this._inputs) {
this._attachToInput(input);
input._attachToInputs();
}
}
}
private _detachFromInputs() {
if (this._attachedToInputs && !this._shouldAttachToInputs()) {
this._attachedToInputs = false;
for (const input of this._inputs) {
this._detachFromInput(input);
input._detachFromInputs();
}
this.onBecomeUnobserved();
plugins.onBecomeUnobserved(this);
}
}
private _attachToInput(input: BaseObservable<any>) {
input._outputs.push(this);
plugins.onAttach(this, input);
}
private _detachFromInput(input: BaseObservable<any>) {
input._outputs.splice(input._outputs.indexOf(this), 1);
plugins.onDetach(this, input);
}
private _addToBatchRecursively() {
if (this._prevVal === UNSET) {
this._prevVal = this._val;
// Add the observable and its outputs in reverse topological order
for (const output of this._outputs) {
output._addToBatchRecursively();
}
batchedObservables.push(this);
}
}
protected static _batch(block: () => void) {
try {
batchDepth++;
if (batchDepth === 1 && batchedUpdater) {
batchedUpdater(block);
} else {
block();
}
} finally {
batchDepth--;
if (batchDepth === 0) {
const observablesToUpdate = batchedObservables;
batchedObservables = [];
// Iterate in reverse order as _addToBatchRecursively() adds them in reverse topological order
observablesToUpdate.reverse().forEach((observable) => {
const prevVal = observable._prevVal;
observable._prevVal = UNSET;
observable._val = observable._evaluate();
const val = observable._val;
if (val !== prevVal) {
for (const listener of observable._listeners.slice()) {
listener(val, prevVal);
}
plugins.onChange(observable, val, prevVal);
}
});
}
}
}
protected static _use(plugin: Plugin) {
plugins.use(plugin);
}
}

View File

@ -0,0 +1,7 @@
export type BatchedUpdater = (block: () => void) => void;
export let batchedUpdater: BatchedUpdater | undefined;
export function setBatchedUpdater(updater: BatchedUpdater | undefined) {
batchedUpdater = updater;
}

View File

@ -0,0 +1,5 @@
export * from "./baseObservable";
export * from "./batchedUpdater";
export * from "./observable";
export * from "./plugin";
export * from "./withPersistence";

View File

@ -0,0 +1,25 @@
export function memoize<T extends any[], U>(func: (args: T) => U): (args: T) => U {
let lastArgs: T | undefined;
let lastResult!: U;
return (args: T) => {
let argsHaveChanged = false;
if (!lastArgs || args.length !== lastArgs.length) {
argsHaveChanged = true;
} else {
for (let i = 0; i < args.length; i++) {
if (args[i] !== lastArgs[i]) {
argsHaveChanged = true;
break;
}
}
}
if (argsHaveChanged) {
lastArgs = args;
lastResult = func(args);
}
return lastResult;
};
}

View File

@ -0,0 +1,211 @@
import { BaseObservable, type Options } from "./baseObservable";
import { memoize } from "./memoize";
import { type Plugin } from "./plugin";
export type ObservableValue<T> = T extends Observable<infer U> ? U : never;
export type ObservableValues<T> = { [K in keyof T]: ObservableValue<T[K]> };
export function observable<T>(
val: T | Observable<T>,
options?: Options
): WritableObservable<T> {
return new WritableObservable(val, options);
}
export function derived<T>(derive: () => T): Observable<T> {
return Observable.compute(derive);
}
export class Observable<T> extends BaseObservable<T> {
protected _valInput: Observable<T> | undefined;
constructor(val: T | Observable<T>, options?: Options) {
super(val instanceof Observable ? val.get() : val, options);
this._updateValInput(val);
}
protected override _evaluate(): T {
return this._valInput ? this._valInput.get() : super._evaluate();
}
select<U>(selector: (val: T) => U | Observable<U>): Observable<U> {
return new DerivedObservable([this], ([val]) => selector(val as T));
}
/**
* @deprecated Use observable.select() instead
*/
transform = this.select;
onlyIf(predicate: (val: T) => boolean): Observable<T | undefined> {
let filteredVal: T | undefined = undefined;
return this.select((val) => {
if (predicate(val)) {
filteredVal = val;
}
return filteredVal;
});
}
default(
defaultVal: NonNullable<T> | Observable<NonNullable<T>>
): Observable<NonNullable<T>> {
return this.select((val) => val ?? defaultVal);
}
as<U extends T>(): Observable<U> {
return this as unknown as Observable<U>;
}
static select<T extends readonly Observable<any>[], U>(
observables: [...T],
selector: (...vals: ObservableValues<T>) => U
): Observable<U> {
return new DerivedObservable(observables, (vals) => selector(...vals));
}
/**
* @deprecated Use Observable.select() instead
*/
static from<T extends Observable<any>[]>(
...observables: T
): Observable<ObservableValues<T>> {
return new DerivedObservable(observables, (values) => values);
}
static merge<T>(observables: Observable<T>[]): Observable<T[]> {
return new DerivedObservable(observables, (values) => values);
}
static latest<T extends Observable<any>[]>(
...observables: T
): Observable<ObservableValue<T[number]>> {
let prevValues: T[] | undefined;
return new DerivedObservable(observables, (values) => {
const val = !prevValues
? values[0]
: values.find((it, index) => it !== prevValues![index])!;
prevValues = values;
return val;
});
}
static compute<U>(compute: () => U): Observable<U> {
return new ComputedObservable(compute);
}
static fromPromise<T, E = undefined>(
promise: Promise<T>,
onError?: (error: any) => E
): Observable<T | E | undefined> {
const obs = observable<T | E | undefined>(undefined);
promise.then(
(val) => obs.set(val),
(e) => onError && obs.set(onError(e))
);
return obs;
}
toPromise(): Promise<T> {
return new Promise((resolve) => {
const unsubscribe = this.subscribe((val) => {
resolve(val);
unsubscribe();
});
});
}
static batch(block: () => void) {
BaseObservable._batch(block);
}
static use(plugin: Plugin) {
BaseObservable._use(plugin);
}
protected _updateValInput(val: T | Observable<T>) {
if (this._valInput !== val) {
if (this._valInput) {
this._removeInput(this._valInput);
this._valInput = undefined;
}
if (val instanceof Observable) {
this._addInput(val);
this._valInput = val;
}
}
}
}
export class WritableObservable<T> extends Observable<T> {
set(val: T | Observable<T>) {
this._updateValInput(val);
Observable.batch(() =>
this._set(val instanceof Observable ? val.get() : val)
);
}
update(updater: (val: T) => T | Observable<T>) {
this.set(updater(this.get()));
}
readOnly(): Observable<T> {
return this;
}
}
class DerivedObservable<T, U extends Observable<any>[]> extends Observable<T> {
private _compute: (vals: ObservableValues<U>) => T | Observable<T>;
private _computeInputs: U;
constructor(
computeInputs: U,
compute: (vals: ObservableValues<U>) => T | Observable<T>
) {
// No need to initialize it as it will be evaluated the first time get() or subscribe() is called
super(undefined as any);
this._compute = memoize(compute);
this._computeInputs = computeInputs;
for (const input of computeInputs) {
this._addInput(input);
}
}
override _evaluate(): T {
const computed = this._compute(
this._computeInputs.map((input) => input.get()) as ObservableValues<U>
);
this._updateValInput(computed);
return computed instanceof Observable ? computed.get() : computed;
}
}
class ComputedObservable<T> extends Observable<T> {
private _compute: () => T;
private _currentInputs = new Set<BaseObservable<any>>();
constructor(compute: () => T) {
// No need to initialize it as it will be evaluated the first time get() or subscribe() is called
super(undefined as any);
this._compute = compute;
}
override _evaluate(): T {
let value!: T;
const inputs = new Set(
BaseObservable._captureInputs(() => (value = this._compute()))
);
inputs.forEach((input) => {
if (!this._currentInputs.has(input)) {
this._addInput(input);
} else {
this._currentInputs.delete(input);
}
});
this._currentInputs.forEach((input) => this._removeInput(input));
this._currentInputs = inputs;
return value;
}
}

View File

@ -0,0 +1,10 @@
import { BaseObservable } from "./baseObservable";
export interface Plugin {
onCreate?(observable: BaseObservable<any>, val: any): void;
onChange?(observable: BaseObservable<any>, val: any, prevVal: any): void;
onBecomeObserved?(observable: BaseObservable<any>): void;
onBecomeUnobserved?(observable: BaseObservable<any>): void;
onAttach?(observable: BaseObservable<any>, input: BaseObservable<any>): void;
onDetach?(observable: BaseObservable<any>, input: BaseObservable<any>): void;
}

View File

@ -0,0 +1,34 @@
import { BaseObservable } from "./baseObservable";
import { type Plugin } from "./plugin";
export class PluginManager {
private _plugins: Plugin[] = [];
use(plugin: Plugin) {
this._plugins.push(plugin);
}
onCreate(observable: BaseObservable<any>, val: any) {
this._plugins.forEach((it) => it.onCreate?.(observable, val));
}
onChange(observable: BaseObservable<any>, val: any, prevVal: any) {
this._plugins.forEach((it) => it.onChange?.(observable, val, prevVal));
}
onBecomeObserved(observable: BaseObservable<any>) {
this._plugins.forEach((it) => it.onBecomeObserved?.(observable));
}
onBecomeUnobserved(observable: BaseObservable<any>) {
this._plugins.forEach((it) => it.onBecomeUnobserved?.(observable));
}
onAttach(observable: BaseObservable<any>, input: BaseObservable<any>) {
this._plugins.forEach((it) => it.onAttach?.(observable, input));
}
onDetach(observable: BaseObservable<any>, input: BaseObservable<any>) {
this._plugins.forEach((it) => it.onDetach?.(observable, input));
}
}

View File

@ -0,0 +1,12 @@
import { WritableObservable, observable } from ".";
import Cookies from "js-cookie";
export const observableWithPersistence = <T>(
cookieKey: string,
defaultValue: T
): WritableObservable<T> => {
const valueFromCookie = Cookies.get(cookieKey) as T | undefined;
const obs = observable(valueFromCookie ?? defaultValue);
obs.subscribe((val) => Cookies.set(cookieKey, val as string));
return obs;
};

2
src/utils/turbo.ts Normal file
View File

@ -0,0 +1,2 @@
export const onLoad = (callback: () => void) =>
document.documentElement.addEventListener("turbo:load", callback);

2
src/utils/urls.ts Normal file
View File

@ -0,0 +1,2 @@
export const getLocalizedUrl = (url: string, locale: string = "en"): string =>
`/${locale}${url}`;

View File

@ -2,9 +2,6 @@
"extends": "astro/tsconfigs/strictest",
"compilerOptions": {
"types": ["bun-types"],
"baseUrl": "./",
"paths": {
"src/*": ["./src/*"]
}
"baseUrl": "./src",
}
}