├── .tool-versions ├── .gitignore ├── public ├── robots.txt ├── favicon.ico ├── assets │ └── fonts │ │ ├── Montserrat-Bold.ttf │ │ ├── Montserrat-Regular.ttf │ │ └── OFL.txt ├── logo.svg ├── index.html ├── global.css └── every-layout.css ├── svelte.config.js ├── web-test-runner.config.js ├── types ├── resource.ts ├── observation.ts └── static.d.ts ├── src ├── App.test.ts ├── ResourceEntry.svelte ├── index.ts ├── utility.ts ├── api │ └── index.ts └── App.svelte ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── base.Dockerfile ├── snowpack.config.mjs ├── README.md ├── package.json ├── tsconfig.json └── .github └── workflows └── developer_ci.yml /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 16.15.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .snowpack 2 | .envrc 3 | .env 4 | build 5 | node_modules -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilvanCodes/resource_observer/main/public/favicon.ico -------------------------------------------------------------------------------- /public/assets/fonts/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilvanCodes/resource_observer/main/public/assets/fonts/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | const autoPreprocess = require('svelte-preprocess'); 2 | 3 | module.exports = { 4 | preprocess: autoPreprocess(), 5 | }; 6 | -------------------------------------------------------------------------------- /public/assets/fonts/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilvanCodes/resource_observer/main/public/assets/fonts/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /web-test-runner.config.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | 3 | module.exports = { 4 | plugins: [require('@snowpack/web-test-runner-plugin')()], 5 | }; 6 | -------------------------------------------------------------------------------- /types/resource.ts: -------------------------------------------------------------------------------- 1 | type Resource = { 2 | url: string; 3 | data: object; 4 | kind: string; 5 | status: string; 6 | createdAt: Date; 7 | }; 8 | 9 | export default Resource; -------------------------------------------------------------------------------- /types/observation.ts: -------------------------------------------------------------------------------- 1 | type Observation = { 2 | createdAt: Date; 3 | id: string; 4 | root: string; 5 | status: string; 6 | webhook: string; 7 | } 8 | 9 | export default Observation; -------------------------------------------------------------------------------- /src/App.test.ts: -------------------------------------------------------------------------------- 1 | import {render} from '@testing-library/svelte'; 2 | import {expect} from 'chai'; 3 | import App from './App.svelte'; 4 | 5 | describe('', () => { 6 | it('renders learn svelte link', () => { 7 | const {getByText} = render(App); 8 | const linkElement = getByText(/learn svelte/i); 9 | expect(document.body.contains(linkElement)); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/ResourceEntry.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |

URL: {resource.url}

9 |

Status: {resource.status}

10 |

Kind: {resource.kind}

11 |
12 | 13 | 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | // set session uuid 5 | window.sessionStorage.setItem('uuid', uuid()) 6 | 7 | var app = new App({ 8 | target: document.body, 9 | }); 10 | 11 | export default app; 12 | 13 | // Hot Module Replacement (HMR) - Remove this snippet to remove HMR. 14 | // Learn more: https://www.snowpack.dev/concepts/hot-module-replacement 15 | if (import.meta.hot) { 16 | import.meta.hot.accept(); 17 | import.meta.hot.dispose(() => { 18 | app.$destroy(); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/utility.ts: -------------------------------------------------------------------------------- 1 | const l = console.log; 2 | 3 | // see here: https://svelte.dev/repl/0ace7a508bd843b798ae599940a91783?version=3.16.7 4 | const clickOutside = (node: Node) => { 5 | const handleClick = (event: Event) => { 6 | node && !node.contains(event.target as Node) && !event.defaultPrevented && node.dispatchEvent(new CustomEvent('clickoutside', {})) 7 | } 8 | 9 | document.addEventListener('click', handleClick, true); 10 | 11 | return { 12 | destroy() { 13 | document.removeEventListener('click', handleClick, true); 14 | } 15 | } 16 | } 17 | 18 | export { l, clickOutside }; -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { l } from "../utility"; 2 | 3 | // API primitives 4 | const api = async (path: string, init: RequestInit) => 5 | fetch(`${import.meta.env.SNOWPACK_PUBLIC_API_URL}${path}`, { 6 | ...init, 7 | headers: { 8 | // uuid is set in src/index.ts 9 | 'X-Session-UUID': window.sessionStorage.getItem('uuid') as string, 10 | ...init.headers 11 | } 12 | }).then(r => { l(r); return r.json(); }).catch(console.error); 13 | 14 | const POST = async (path: string, data: object) => api(path, { method: 'POST', body: JSON.stringify(data) }) 15 | const GET = async (path: string) => api(path, { method: 'GET' }) 16 | 17 | export { POST, GET }; 18 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster 2 | ARG VARIANT=16-bullseye 3 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} 4 | 5 | # [Optional] Uncomment this section to install additional OS packages. 6 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 7 | # && apt-get -y install --no-install-recommends 8 | 9 | # [Optional] Uncomment if you want to install an additional version of node using nvm 10 | # ARG EXTRA_NODE_VERSION=10 11 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 12 | 13 | # [Optional] Uncomment if you want to install more global node modules 14 | # RUN su node -c "npm install -g " 15 | -------------------------------------------------------------------------------- /snowpack.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import("snowpack").SnowpackUserConfig } */ 2 | export default { 3 | mount: { 4 | public: { url: '/', static: true }, 5 | src: { url: '/dist' }, 6 | }, 7 | plugins: [ 8 | '@snowpack/plugin-svelte', 9 | '@snowpack/plugin-dotenv', 10 | [ 11 | '@snowpack/plugin-typescript', 12 | { 13 | /* Yarn PnP workaround: see https://www.npmjs.com/package/@snowpack/plugin-typescript */ 14 | ...(process.versions.pnp ? { tsc: 'yarn pnpify tsc' } : {}), 15 | }, 16 | ], 17 | ], 18 | routes: [ 19 | /* Enable an SPA Fallback in development: */ 20 | // {"match": "routes", "src": ".*", "dest": "/index.html"}, 21 | ], 22 | optimize: { 23 | /* Example: Bundle your final build: */ 24 | // "bundle": true, 25 | }, 26 | packageOptions: { 27 | /* ... */ 28 | }, 29 | devOptions: { 30 | /* ... */ 31 | }, 32 | buildOptions: { 33 | /* ... */ 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WhatMeal 2 | 3 | > Started from `npx create-snowpack-app whatmeal --template @snowpack/app-template-svelte-typescript` 4 | 5 | Super duper cool with [Dark] and [Snowpack] and [Svelte]. 6 | 7 | [Dark]: https://darklang.com 8 | [Snowpack]: https://www.snowpack.dev 9 | [Svelte]: https://svelte.dev 10 | 11 | ## Pitfalls sofar 12 | 13 | Had to use `import.meta.env.` ([which I stumbled upon here]) instead of `__SNOWPACK_ENV__` as described [in the docs]. 14 | No clue why it fails, maybe due to Typescript? 15 | 16 | [which I stumbled upon here]: https://bhirmbani.medium.com/managing-multiple-environment-in-snowpack-18ba46ed78b 17 | [in the docs]: https://www.snowpack.dev/reference/environment-variables 18 | 19 | ## Quirks 20 | 21 | See `package.json` and `index.html` which had to be ajusted for a variable root directory configured via `SNOWPACK_PUBLIC_BASE_URL` in `.env` [due to this]. 22 | 23 | [due to this]: https://docs.darklang.com/static-assets/#configure-your-client -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "snowpack dev", 4 | "build": "SNOWPACK_PUBLIC_BASE_URL=DARK_STATIC_ASSETS_BASE_URL snowpack build", 5 | "test": "web-test-runner \"src/**/*.test.ts\"", 6 | "publish": "dark-cli build --canvas $DARK_CLI_CANVAS" 7 | }, 8 | "dependencies": { 9 | "svelte": "^3.48.0", 10 | "uuid": "^8.3.2" 11 | }, 12 | "devDependencies": { 13 | "@snowpack/plugin-dotenv": "^2.2.0", 14 | "@snowpack/plugin-svelte": "^3.7.0", 15 | "@snowpack/plugin-typescript": "^1.2.1", 16 | "@snowpack/web-test-runner-plugin": "^0.2.2", 17 | "@testing-library/svelte": "^3.1.1", 18 | "@tsconfig/svelte": "^3.0.0", 19 | "@types/chai": "^4.3.1", 20 | "@types/mocha": "^9.1.1", 21 | "@types/snowpack-env": "^2.3.4", 22 | "@types/uuid": "^8.3.4", 23 | "@web/test-runner": "^0.13.27", 24 | "chai": "^4.3.6", 25 | "snowpack": "^3.8.8", 26 | "svelte-preprocess": "^4.10.6", 27 | "typescript": "^4.6.4" 28 | } 29 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "include": [ 4 | "src", 5 | "types" 6 | ], 7 | "compilerOptions": { 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "jsx": "preserve", 11 | "baseUrl": "./", 12 | /* paths - import rewriting/resolving */ 13 | "paths": { 14 | // If you configured any Snowpack aliases, add them here. 15 | // Add this line to get types for streaming imports (packageOptions.source="remote"): 16 | // "*": [".snowpack/types/*"] 17 | // More info: https://www.snowpack.dev/guides/streaming-imports 18 | }, 19 | /* noEmit - Snowpack builds (emits) files, not tsc. */ 20 | "noEmit": true, 21 | /* Additional Options */ 22 | "strict": true, 23 | "skipLibCheck": true, 24 | "types": [ 25 | "mocha", 26 | "snowpack-env" 27 | ], 28 | "forceConsistentCasingInFileNames": true, 29 | "resolveJsonModule": true, 30 | "useDefineForClassFields": true, 31 | "allowSyntheticDefaultImports": true, 32 | "importsNotUsedAsValues": "error", 33 | "target": "ES6" 34 | } 35 | } -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/javascript-node 3 | { 4 | "name": "Node.js", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 18, 16, 14. 8 | // Append -bullseye or -buster to pin to an OS version. 9 | // Use -bullseye variants on local arm64/Apple Silicon. 10 | "args": { 11 | "VARIANT": "16-bullseye" 12 | } 13 | }, 14 | // Configure tool-specific properties. 15 | "customizations": { 16 | // Configure properties specific to VS Code. 17 | "vscode": { 18 | // Add the IDs of extensions you want installed when the container is created. 19 | "extensions": [ 20 | "dbaeumer.vscode-eslint" 21 | ] 22 | } 23 | }, 24 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 25 | // "forwardPorts": [], 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | // "postCreateCommand": "yarn install", 28 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 29 | "remoteUser": "node", 30 | "features": { 31 | "ghcr.io/devcontainers/features/docker-from-docker:1": { 32 | "version": "latest" 33 | }, 34 | "ghcr.io/devcontainers/features/git:1": { 35 | "version": "latest" 36 | }, 37 | "ghcr.io/devcontainers/features/github-cli:1": { 38 | "version": "latest" 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /types/static.d.ts: -------------------------------------------------------------------------------- 1 | /* Use this file to declare any custom file extensions for importing */ 2 | /* Use this folder to also add/extend a package d.ts file, if needed. */ 3 | 4 | /* CSS MODULES */ 5 | declare module '*.module.css' { 6 | const classes: { [key: string]: string }; 7 | export default classes; 8 | } 9 | declare module '*.module.scss' { 10 | const classes: { [key: string]: string }; 11 | export default classes; 12 | } 13 | declare module '*.module.sass' { 14 | const classes: { [key: string]: string }; 15 | export default classes; 16 | } 17 | declare module '*.module.less' { 18 | const classes: { [key: string]: string }; 19 | export default classes; 20 | } 21 | declare module '*.module.styl' { 22 | const classes: { [key: string]: string }; 23 | export default classes; 24 | } 25 | 26 | /* CSS */ 27 | declare module '*.css'; 28 | declare module '*.scss'; 29 | declare module '*.sass'; 30 | declare module '*.less'; 31 | declare module '*.styl'; 32 | 33 | /* IMAGES */ 34 | declare module '*.svg' { 35 | const ref: string; 36 | export default ref; 37 | } 38 | declare module '*.bmp' { 39 | const ref: string; 40 | export default ref; 41 | } 42 | declare module '*.gif' { 43 | const ref: string; 44 | export default ref; 45 | } 46 | declare module '*.jpg' { 47 | const ref: string; 48 | export default ref; 49 | } 50 | declare module '*.jpeg' { 51 | const ref: string; 52 | export default ref; 53 | } 54 | declare module '*.png' { 55 | const ref: string; 56 | export default ref; 57 | } 58 | 59 | /* CUSTOM: ADD YOUR OWN HERE */ 60 | 61 | declare namespace svelte.JSX { 62 | interface HTMLAttributes { 63 | onclickoutside?: EventHandler | undefined | null; 64 | } 65 | } -------------------------------------------------------------------------------- /.github/workflows/developer_ci.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Developer CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: [main] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v2 22 | 23 | - name: Setup node 24 | id: cache-npm 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: "16" 28 | cache: "npm" 29 | 30 | - name: Clean install dependencies 31 | run: npm ci --prefer-offline 32 | 33 | - name: Build project 34 | env: 35 | SNOWPACK_PUBLIC_API_URL: ${{ secrets.SNOWPACK_PUBLIC_API_URL }} 36 | run: npm run build 37 | 38 | - name: Upload build 39 | uses: actions/upload-artifact@v2 40 | with: 41 | name: build-dir 42 | path: build 43 | 44 | deploy: 45 | runs-on: ubuntu-latest 46 | needs: build 47 | 48 | steps: 49 | - name: Check cache for dark-cli 50 | id: cache-dark-cli 51 | uses: actions/cache@v2 52 | with: 53 | path: ./dark-cli-linux 54 | key: dark-cli 55 | 56 | - name: Download dark-cli 57 | if: ${{ !steps.cache-dark-cli.outputs.cache-hit }} 58 | run: | 59 | curl -O https://dark-cli.storage.googleapis.com/latest/dark-cli-linux 60 | chmod +x ./dark-cli-linux 61 | 62 | - name: Download build 63 | uses: actions/download-artifact@v2 64 | with: 65 | name: build-dir 66 | path: build 67 | 68 | - name: Deploy to Dark 69 | env: 70 | DARK_CLI_USER: ${{ secrets.DARK_CLI_USER }} 71 | DARK_CLI_PASSWORD: ${{ secrets.DARK_CLI_PASSWORD }} 72 | DARK_CLI_CANVAS: ${{ secrets.DARK_CLI_CANVAS }} 73 | run: ./dark-cli-linux build --canvas $DARK_CLI_CANVAS 74 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ResourceObserver 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 38 | 39 | 40 | 41 | 42 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.devcontainer/base.Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster 2 | ARG VARIANT=16-bullseye 3 | FROM node:${VARIANT} 4 | 5 | # [Option] Install zsh 6 | ARG INSTALL_ZSH="true" 7 | # [Option] Upgrade OS packages to their latest versions 8 | ARG UPGRADE_PACKAGES="true" 9 | 10 | # Install needed packages, yarn, nvm and setup non-root user. Use a separate RUN statement to add your own dependencies. 11 | ARG USERNAME=node 12 | ARG USER_UID=1000 13 | ARG USER_GID=$USER_UID 14 | ARG NPM_GLOBAL=/usr/local/share/npm-global 15 | ENV NVM_DIR=/usr/local/share/nvm 16 | ENV NVM_SYMLINK_CURRENT=true \ 17 | PATH=${NPM_GLOBAL}/bin:${NVM_DIR}/current/bin:${PATH} 18 | COPY library-scripts/*.sh library-scripts/*.env /tmp/library-scripts/ 19 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 20 | # Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131 21 | && apt-get purge -y imagemagick imagemagick-6-common \ 22 | # Install common packages, non-root user, update yarn and install nvm 23 | && bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ 24 | # Install yarn, nvm 25 | && rm -rf /opt/yarn-* /usr/local/bin/yarn /usr/local/bin/yarnpkg \ 26 | && bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "none" "${USERNAME}" \ 27 | # Configure global npm install location, use group to adapt to UID/GID changes 28 | && if ! cat /etc/group | grep -e "^npm:" > /dev/null 2>&1; then groupadd -r npm; fi \ 29 | && usermod -a -G npm ${USERNAME} \ 30 | && umask 0002 \ 31 | && mkdir -p ${NPM_GLOBAL} \ 32 | && touch /usr/local/etc/npmrc \ 33 | && chown ${USERNAME}:npm ${NPM_GLOBAL} /usr/local/etc/npmrc \ 34 | && chmod g+s ${NPM_GLOBAL} \ 35 | && npm config -g set prefix ${NPM_GLOBAL} \ 36 | && sudo -u ${USERNAME} npm config -g set prefix ${NPM_GLOBAL} \ 37 | # Install eslint 38 | && su ${USERNAME} -c "umask 0002 && npm install -g eslint" \ 39 | && npm cache clean --force > /dev/null 2>&1 \ 40 | # Install python-is-python3 on bullseye to prevent node-gyp regressions 41 | && . /etc/os-release \ 42 | && if [ "${VERSION_CODENAME}" = "bullseye" ]; then apt-get -y install --no-install-recommends python-is-python3; fi \ 43 | # Clean up 44 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /root/.gnupg /tmp/library-scripts 45 | 46 | # [Optional] Uncomment this section to install additional OS packages. 47 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 48 | # && apt-get -y install --no-install-recommends 49 | 50 | # [Optional] Uncomment if you want to install an additional version of node using nvm 51 | # ARG EXTRA_NODE_VERSION=10 52 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 53 | 54 | # [Optional] Uncomment if you want to install more global node modules 55 | # RUN su node -c "npm install -g "" -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 |
49 |

Resource Observer

50 |
51 | 52 | 53 | 61 | 62 | 65 | 66 | 67 |

68 | The resource observer starts from the index.html and checks all internal 69 | and external resources for status code 2xx. Other status codes are 70 | reported as broken. The observation takes place at a rate of 60 71 | resources per minute. I.e. it may take up to one minute to see the first 72 | results. The observation is limited to 1000 resources in total. 73 |

74 | 75 | 81 | 82 | {#if observedResources} 83 |
84 |

Status: {observedResources.status}

85 |
86 | 87 |
    88 | {#each observedResources.results as resource} 89 |
  • 90 | {/each} 91 |
92 | {:else} 93 |

Enter domain to observe.

94 | {/if} 95 |
96 | -------------------------------------------------------------------------------- /public/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-blue: #53768A; 3 | --color-orange: #E09329; 4 | --color-white: #E4DAD9; 5 | --border-radius-base: var(--s-4); 6 | --font-size-base: var(--s1); 7 | --font-size-base-plus: var(--s2); 8 | --font-size-big: var(--s3); 9 | --font-size-big-plus: var(--s5); 10 | --flex-grow-max: 999; 11 | } 12 | 13 | * { 14 | font-family: "Montserrat-Regular", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; 15 | } 16 | 17 | h1, 18 | h2, 19 | h3, 20 | h4, 21 | h5, 22 | h6 { 23 | font-family: "Montserrat-Bold", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; 24 | } 25 | 26 | h1 { 27 | font-size: var(--font-size-big-plus); 28 | } 29 | 30 | h2 { 31 | font-size: var(--font-size-big); 32 | } 33 | 34 | body { 35 | background: var(--color-white); 36 | } 37 | 38 | html, 39 | body, 40 | main { 41 | block-size: 100%; 42 | } 43 | 44 | button { 45 | border-radius: var(--border-radius-base); 46 | border-style: none; 47 | padding: var(--s-1); 48 | } 49 | 50 | button.primary { 51 | background: var(--color-orange); 52 | min-inline-size: fit-content; 53 | } 54 | 55 | button.secondary { 56 | background: var(--color-blue); 57 | color: var(--color-white); 58 | } 59 | 60 | button:hover { 61 | cursor: pointer; 62 | opacity: 0.9; 63 | } 64 | 65 | input { 66 | border-radius: var(--border-radius-base); 67 | border-style: none; 68 | padding: var(--s-1); 69 | } 70 | 71 | .elc-box { 72 | border-color: var(--color-blue); 73 | border-radius: var(--border-radius-base); 74 | } 75 | 76 | .spin { 77 | animation-name: spin; 78 | animation-duration: 1s; 79 | animation-iteration-count: infinite; 80 | animation-timing-function: ease-in-out; 81 | display: inline-block; 82 | } 83 | 84 | @keyframes spin { 85 | from { 86 | transform: rotate(0deg); 87 | } 88 | 89 | to { 90 | transform: rotate(360deg); 91 | } 92 | } 93 | 94 | .pulse { 95 | animation-name: pulse; 96 | animation-duration: 1s; 97 | animation-iteration-count: infinite; 98 | animation-direction: alternate; 99 | animation-timing-function: ease-in-out; 100 | display: inline-block; 101 | } 102 | 103 | @keyframes pulse { 104 | from { 105 | transform: scale(1); 106 | } 107 | 108 | to { 109 | transform: scale(calc(1 / var(--ratio) * 2)); 110 | } 111 | } 112 | 113 | .text-align\:center { 114 | text-align: center; 115 | } 116 | 117 | .font-size\:base { 118 | font-size: var(--font-size-base); 119 | } 120 | 121 | .font-size\:base-plus { 122 | font-size: var(--font-size-base-plus); 123 | } 124 | 125 | .font-size\:big { 126 | font-size: var(--font-size-big); 127 | } 128 | 129 | .font-size\:big-plus { 130 | font-size: var(--font-size-big-plus); 131 | } 132 | 133 | .word-break\:break-word { 134 | word-break: break-word; 135 | } 136 | 137 | .pulse-on-click { 138 | transition: transform var(--t-2) ease-out; 139 | transform-origin: center; 140 | } 141 | 142 | .pulse-on-click:active { 143 | transform: scale(var(--v-1)); 144 | transition: transform var(--t-5) ease-in; 145 | } 146 | 147 | .glow-on-click { 148 | transition: color var(--t-5) ease-out; 149 | } 150 | 151 | .glow-on-click:active { 152 | color: var(--color-blue); 153 | transition: color var(--zero) linear; 154 | } 155 | 156 | .flex-grow\:max { 157 | flex-grow: var(--flex-grow-max); 158 | } 159 | 160 | .flex-grow\:1 { 161 | flex-grow: 1; 162 | } 163 | 164 | .display\:grid { 165 | display: grid; 166 | } 167 | 168 | .display\:inline-block { 169 | display: inline-block; 170 | } 171 | 172 | .transform\:rotateZ\:90 { 173 | transform: rotateZ(90deg); 174 | } -------------------------------------------------------------------------------- /public/assets/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2012, Julieta Ulanovsky (julieta.ulanovsky@gmail.com), with Reserved Font Names 'Montserrat' 2 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 3 | This license is copied below, and is also available with a FAQ at: 4 | http://scripts.sil.org/OFL 5 | 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION & CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | -------------------------------------------------------------------------------- /public/every-layout.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ratio: 1.22; 3 | 4 | /* Ratios for maintaining visual consistency. */ 5 | --s-9: calc(var(--s-8) / var(--ratio)); 6 | --s-8: calc(var(--s-7) / var(--ratio)); 7 | --s-7: calc(var(--s-6) / var(--ratio)); 8 | --s-6: calc(var(--s-5) / var(--ratio)); 9 | --s-5: calc(var(--s-4) / var(--ratio)); 10 | --s-4: calc(var(--s-3) / var(--ratio)); 11 | --s-3: calc(var(--s-2) / var(--ratio)); 12 | --s-2: calc(var(--s-1) / var(--ratio)); 13 | --s-1: calc(var(--s0) / var(--ratio)); 14 | --s0: 1rem; 15 | --s1: calc(var(--s0) * var(--ratio)); 16 | --s2: calc(var(--s1) * var(--ratio)); 17 | --s3: calc(var(--s2) * var(--ratio)); 18 | --s4: calc(var(--s3) * var(--ratio)); 19 | --s5: calc(var(--s4) * var(--ratio)); 20 | --s6: calc(var(--s5) * var(--ratio)); 21 | --s7: calc(var(--s6) * var(--ratio)); 22 | --s8: calc(var(--s7) * var(--ratio)); 23 | --s9: calc(var(--s8) * var(--ratio)); 24 | 25 | /* Ratios for maintaining timely consistency. */ 26 | --t-9: calc(var(--t-8) / var(--ratio)); 27 | --t-8: calc(var(--t-7) / var(--ratio)); 28 | --t-7: calc(var(--t-6) / var(--ratio)); 29 | --t-6: calc(var(--t-5) / var(--ratio)); 30 | --t-5: calc(var(--t-4) / var(--ratio)); 31 | --t-4: calc(var(--t-3) / var(--ratio)); 32 | --t-3: calc(var(--t-2) / var(--ratio)); 33 | --t-2: calc(var(--t-1) / var(--ratio)); 34 | --t-1: calc(var(--t0) / var(--ratio)); 35 | --t0: 1000ms; 36 | --t1: calc(var(--t0) * var(--ratio)); 37 | --t2: calc(var(--t1) * var(--ratio)); 38 | --t3: calc(var(--t2) * var(--ratio)); 39 | --t4: calc(var(--t3) * var(--ratio)); 40 | --t5: calc(var(--t4) * var(--ratio)); 41 | --t6: calc(var(--t5) * var(--ratio)); 42 | --t7: calc(var(--t6) * var(--ratio)); 43 | --t8: calc(var(--t7) * var(--ratio)); 44 | --t9: calc(var(--t8) * var(--ratio)); 45 | 46 | /* Ratios for maintaining general consistency. */ 47 | --v-9: calc(var(--v-8) / var(--ratio)); 48 | --v-8: calc(var(--v-7) / var(--ratio)); 49 | --v-7: calc(var(--v-6) / var(--ratio)); 50 | --v-6: calc(var(--v-5) / var(--ratio)); 51 | --v-5: calc(var(--v-4) / var(--ratio)); 52 | --v-4: calc(var(--v-3) / var(--ratio)); 53 | --v-3: calc(var(--v-2) / var(--ratio)); 54 | --v-2: calc(var(--v-1) / var(--ratio)); 55 | --v-1: calc(var(--v0) / var(--ratio)); 56 | --v0: 1; 57 | --v1: calc(var(--v0) * var(--ratio)); 58 | --v2: calc(var(--v1) * var(--ratio)); 59 | --v3: calc(var(--v2) * var(--ratio)); 60 | --v4: calc(var(--v3) * var(--ratio)); 61 | --v5: calc(var(--v4) * var(--ratio)); 62 | --v6: calc(var(--v5) * var(--ratio)); 63 | --v7: calc(var(--v6) * var(--ratio)); 64 | --v8: calc(var(--v7) * var(--ratio)); 65 | --v9: calc(var(--v8) * var(--ratio)); 66 | 67 | /* Zero value*/ 68 | --zero: 0; 69 | 70 | /* Measure width (characters per line) */ 71 | --measure: 60ch; 72 | 73 | /* Finest unit possible */ 74 | --finest: 1px; 75 | } 76 | 77 | * { 78 | /* In general calculate from border-box. */ 79 | box-sizing: border-box; 80 | 81 | /* Clear default padding and margin. */ 82 | margin: 0; 83 | padding: 0; 84 | 85 | /* In general cap to nicely readable width. */ 86 | max-inline-size: var(--measure); 87 | } 88 | 89 | /* Exceptions made to above max-width rule. */ 90 | html, 91 | body, 92 | div, 93 | header, 94 | nav, 95 | main, 96 | footer { 97 | max-inline-size: none; 98 | } 99 | 100 | img { 101 | max-block-size: 100%; 102 | } 103 | 104 | /* BOX-LAYOUT */ 105 | .elc-box { 106 | --box-padding: var(--s0); 107 | --box-border-width: var(--finest); 108 | padding: var(--box-padding); 109 | border: var(--box-border-width) solid; 110 | } 111 | 112 | .elc-box * { 113 | color: inherit; 114 | } 115 | 116 | /* CENTER-LAYOUT */ 117 | .elc-center { 118 | --center-padding-inline: var(--zero); 119 | box-sizing: content-box; 120 | margin-inline: auto; 121 | display: flex; 122 | flex-direction: column; 123 | align-items: center; 124 | padding-inline: var(--center-padding-inline); 125 | } 126 | 127 | /* CLUSTER-LAYOUT */ 128 | .elc-cluster { 129 | --cluster-gap: var(--s0); 130 | --cluster-justify-content: flex-start; 131 | --cluster-align-items: center; 132 | display: flex; 133 | flex-wrap: wrap; 134 | gap: var(--cluster-gap); 135 | justify-content: var(--cluster-justify-content); 136 | align-items: var(--cluster-align-items); 137 | } 138 | 139 | /* COVER-LAYOUT */ 140 | .elc-cover { 141 | --cover-padding: var(--s0); 142 | --cover-margin: var(--s0); 143 | --cover-min-block-size: 100%; 144 | display: flex; 145 | flex-direction: column; 146 | min-block-size: var(--cover-min-block-size); 147 | padding: var(--cover-padding); 148 | } 149 | 150 | .elc-cover>* { 151 | margin-block: var(--cover-margin); 152 | } 153 | 154 | /* If just on child exists, it is main element. */ 155 | .elc-cover> :only-child { 156 | margin-block: auto; 157 | } 158 | 159 | /* If two children exist, it is heading and main elements. */ 160 | .elc-cover> :last-child:nth-child(2) { 161 | margin-block: auto; 162 | } 163 | 164 | .elc-cover> :first-child:nth-last-child(2) { 165 | margin-block-start: 0; 166 | } 167 | 168 | /* If three children exist, it is heading, main and footer elements. */ 169 | .elc-cover> :nth-child(2):nth-last-child(2) { 170 | margin-block: auto; 171 | } 172 | 173 | .elc-cover> :first-child:nth-last-child(3) { 174 | margin-block-start: 0; 175 | } 176 | 177 | .elc-cover> :last-child::nth-child(3) { 178 | margin-block-end: 0; 179 | } 180 | 181 | /* FRAME-LAYOUT */ 182 | .elc-frame { 183 | --frame-n: 16; 184 | --frame-d: 9; 185 | aspect-ratio: var(--frame-n) / var(--frame-d); 186 | overflow: hidden; 187 | display: flex; 188 | justify-content: center; 189 | align-items: center; 190 | } 191 | 192 | .elc-frame>img, 193 | .elc-frame>video { 194 | inline-size: 100%; 195 | block-size: 100%; 196 | object-fit: cover; 197 | } 198 | 199 | /* GRID-LAYOUT */ 200 | .elc-grid { 201 | --grid-grid-gap: var(--s0); 202 | --grid-column-min-width: var(--s5); 203 | display: grid; 204 | grid-gap: var(--grid-grid-gap); 205 | } 206 | 207 | .elc-grid { 208 | grid-template-columns: repeat(auto-fit, 209 | minmax(min(var(--grid-column-min-width), 100%), 1fr)); 210 | } 211 | 212 | /* IMPOSTER-LAYOUT */ 213 | .elc-imposter { 214 | --imposter-position: absolute; 215 | --imposter-margin: var(--s0); 216 | position: var(--imposter-position); 217 | inset-block-start: 50%; 218 | inset-inline-start: 50%; 219 | transform: translate(-50%, -50%); 220 | max-inline-size: calc(100% - 2 * var(--imposter-margin)); 221 | max-block-size: calc(100% - 2 * var(--imposter-margin)); 222 | overflow: auto; 223 | } 224 | 225 | /* REEL-LAYOUT */ 226 | .elc-reel { 227 | --reel-block-size: auto; 228 | --reel-padding: var(--s0); 229 | --reel-gap: var(--s0); 230 | display: flex; 231 | overflow-x: auto; 232 | block-size: var(--reel-block-size); 233 | gap: var(--reel-gap); 234 | } 235 | 236 | .elc-reel>* { 237 | margin-block: var(--reel-padding); 238 | } 239 | 240 | .elc-reel> :first-child { 241 | margin-inline-start: var(--reel-padding); 242 | } 243 | 244 | .elc-reel>:last-child { 245 | margin-inline-end: var(--reel-padding); 246 | } 247 | 248 | .elc-reel>img { 249 | block-size: 100%; 250 | inline-size: auto; 251 | } 252 | 253 | /* SIDEBAR-LAYOUT */ 254 | .elc-sidebar { 255 | /* Flex basis for sidebar. */ 256 | --sidebar-flex-basis: initial; 257 | /* Minimum inline size of main content */ 258 | --sidebar-min-inline-size: 50%; 259 | --sidebar-gap: var(--s0); 260 | /* Can be set to 'row-reverse' to switch sidebar and main content. */ 261 | --sidebar-flex-direction: row; 262 | display: flex; 263 | flex-wrap: wrap; 264 | flex-direction: var(--sidebar-flex-direction); 265 | gap: var(--sidebar-gap); 266 | } 267 | 268 | .elc-sidebar> :first-child { 269 | flex-basis: var(--sidebar-flex-basis); 270 | flex-grow: 1; 271 | } 272 | 273 | .elc-sidebar> :last-child { 274 | flex-basis: 0; 275 | flex-grow: 999; 276 | min-inline-size: var(--sidebar-min-inline-size); 277 | } 278 | 279 | /* STACK-LAYOUT */ 280 | /* 281 | * Not directly nestable due to CSS variable override of outer stack. 282 | * Use one wrapping element between those with .elc-stack class. 283 | */ 284 | .elc-stack { 285 | --stack-margin: var(--s0); 286 | /* Should be set to 'auto' when splitting is desired. 287 | * Splitting occurs when the stack has exactly two elements. 288 | */ 289 | --stack-split: var(--zero); 290 | display: flex; 291 | flex-direction: column; 292 | justify-content: flex-start; 293 | } 294 | 295 | .elc-stack> :first-child:nth-last-child(2) { 296 | margin-block-end: var(--stack-split); 297 | } 298 | 299 | .elc-stack>*+* { 300 | margin-top: var(--stack-margin); 301 | } 302 | 303 | .elc-stack:only-child { 304 | block-size: 100%; 305 | } 306 | 307 | /* SWITCHER-LAYOUT */ 308 | .elc-switcher { 309 | --switcher-gap: var(--s0); 310 | --switcher-threshold: var(--measure); 311 | display: flex; 312 | flex-wrap: wrap; 313 | gap: var(--switcher-gap); 314 | 315 | } 316 | 317 | .elc-switcher>* { 318 | flex-grow: 1; 319 | flex-basis: calc((var(--switcher-threshold) - 100%) * 999); 320 | } 321 | 322 | /* ICON */ 323 | /* Gives icons the size of a box with edge length of lowercase letter 'x'. 324 | * As it is square, ther is no need for logical properties. 325 | */ 326 | .elc-icon { 327 | width: 1ex; 328 | height: 1ex; 329 | } --------------------------------------------------------------------------------