Muchos stuff
|
@ -1,8 +1,4 @@
|
|||
{
|
||||
"editor.rulers": [100],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true,
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"editor.tabSize": 2
|
||||
}
|
||||
|
|
57
README.md
|
@ -1,54 +1,7 @@
|
|||
# Astro Starter Kit: Basics
|
||||
# Accord’s 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/)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
13
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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}
|
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 56 KiB |
|
@ -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>
|
|
@ -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>→</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>
|
|
@ -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 |
|
@ -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">Accord’s 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 website’s 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>
|
||||
Accord’s 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,4 +0,0 @@
|
|||
import "sanitize.css";
|
||||
import "src/styles/global.css";
|
||||
import "src/styles/reset.css";
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import AppLayout from "layouts/AppLayout.astro";
|
||||
---
|
||||
|
||||
<AppLayout title="Accord’s Library">
|
||||
<main slot="mainPanel">
|
||||
<div id="header">
|
||||
<div>
|
||||
<Icon name="accords" />
|
||||
</div>
|
||||
<h1>Accord’s Library</h1>
|
||||
<h2 class="angelic">Discover • Analyze • Translate • Archive</h2>
|
||||
</div>
|
||||
|
||||
<div class="formatted">
|
||||
<p>
|
||||
Accord’s Library aims at gathering and archiving all of Yoko Taro’s
|
||||
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 wiki’s
|
||||
contributors a simple way to access these records filled with stories,
|
||||
artworks, and knowledge.
|
||||
</p><p>
|
||||
We are a small group of Yoko Taro’s 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>
|
|
@ -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 Yokoverse’s 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>
|
|
@ -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;
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
:where(button) {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
border: initial;
|
||||
padding: initial;
|
||||
margin: initial;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference path="../../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
|
@ -0,0 +1,4 @@
|
|||
declare namespace astroHTML.JSX {
|
||||
interface HTMLAttributes {}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// https://turbo.hotwired.dev/reference/drive
|
||||
declare namespace Turbo {
|
||||
const cache = {
|
||||
clear: () => null,
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export enum CookieNames {
|
||||
MENU_PANEL_REDUCED = "menuPanelReduced",
|
||||
THEME_COLOR = "themeColor",
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export type BatchedUpdater = (block: () => void) => void;
|
||||
|
||||
export let batchedUpdater: BatchedUpdater | undefined;
|
||||
|
||||
export function setBatchedUpdater(updater: BatchedUpdater | undefined) {
|
||||
batchedUpdater = updater;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export * from "./baseObservable";
|
||||
export * from "./batchedUpdater";
|
||||
export * from "./observable";
|
||||
export * from "./plugin";
|
||||
export * from "./withPersistence";
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export const onLoad = (callback: () => void) =>
|
||||
document.documentElement.addEventListener("turbo:load", callback);
|
|
@ -0,0 +1,2 @@
|
|||
export const getLocalizedUrl = (url: string, locale: string = "en"): string =>
|
||||
`/${locale}${url}`;
|
|
@ -2,9 +2,6 @@
|
|||
"extends": "astro/tsconfigs/strictest",
|
||||
"compilerOptions": {
|
||||
"types": ["bun-types"],
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"src/*": ["./src/*"]
|
||||
}
|
||||
"baseUrl": "./src",
|
||||
}
|
||||
}
|
||||
|
|