| @@ -1,5 +1,5 @@ | |||
| import { nextTick, inject, provide, watch, type InjectionKey, onBeforeUnmount, watchEffect, onMounted, type Ref, type CSSProperties, defineComponent, ref } from "vue"; | |||
| import { h as djh } from "@/util.ts"; | |||
| import { addCSS, css, h as djh } from "@/util.ts"; | |||
| type TooltipContext = { | |||
| show: (newText: string, x: number, y: number) => void, | |||
| @@ -8,9 +8,42 @@ type TooltipContext = { | |||
| const tooltipContext = Symbol('tooltip') as InjectionKey<TooltipContext>; | |||
| const tooltipStyles = css` | |||
| .tooltip-container { | |||
| display: inline-block; | |||
| } | |||
| .tooltip-carrier { | |||
| opacity: 0; | |||
| display: block; | |||
| pointer-events: none; | |||
| background-color: black; | |||
| border: white solid 1px; | |||
| color: white; | |||
| padding: 10px; | |||
| position: absolute; | |||
| z-index: 1; | |||
| overflow: hidden; | |||
| height: 0; | |||
| width: 0; | |||
| position: absolute; | |||
| transition: opacity 200ms, height 200ms, width 200ms; | |||
| .text-carrier { | |||
| position: absolute; | |||
| width: 350px; | |||
| font-size: 16px; | |||
| font-family: "Roboto", serif; | |||
| display: block; | |||
| overflow: hidden; | |||
| } | |||
| }`; | |||
| export function setupTooltip(options: { carrier: Ref<HTMLElement | null> }) { | |||
| const { carrier } = options; | |||
| addCSS('tooltip-carrier', tooltipStyles); | |||
| watchEffect(() => { | |||
| if (carrier.value) { | |||
| carrier.value.classList.add('tooltip-carrier'); | |||
| @@ -82,6 +115,8 @@ export default defineComponent({ | |||
| setup(props, { slots, attrs }) { | |||
| const tooltip = inject(tooltipContext, () => { throw new Error('No tooltip context'); }, true); | |||
| onBeforeUnmount(() => tooltip.hide()); | |||
| return () => <> | |||
| <div class="tooltip-container" | |||
| {...attrs} | |||
| @@ -1,7 +1,7 @@ | |||
| export type DJAPIEndpoint = "/rp-articles"; | |||
| export interface DJAPIResultMap extends Record<DJAPIEndpoint, unknown> { | |||
| "/rp-articles": { slug: string; name: string }[]; | |||
| "/rp-articles": { slug: string; title: string, tags?: string[] }[]; | |||
| } | |||
| export type DJAPIResult = DJAPIResultMap[DJAPIEndpoint]; | |||
| @@ -0,0 +1 @@ | |||
| export function setupDevtoolsPlugin() {} | |||
| @@ -17,17 +17,17 @@ export default defineComponent({ | |||
| step: 0.1, | |||
| }, | |||
| { | |||
| name: "Armour, Natural Dessicated Thyroid", | |||
| name: 'Natural Dessicated Thyroid ("Armour")', | |||
| mpg: 60, | |||
| unit: "mg", | |||
| }, | |||
| { | |||
| name: 'Liothyronine (Triiodothyronine, "Cytomel/Cynomel", T3)', | |||
| name: 'Liothyronine (T3 - "Cytomel/Cynomel")', | |||
| mpg: MPG_T3_SYN, | |||
| unit: "mcg", | |||
| }, | |||
| { | |||
| name: "Levothyroxine (Thyroxine, T4)", | |||
| name: "Levothyroxine (T4)", | |||
| mpg: MPG_T4_SYN, | |||
| unit: "mcg", | |||
| }, | |||
| @@ -67,7 +67,7 @@ export default defineComponent({ | |||
| {inputDefs.map((_) => ( | |||
| <tr key={_.name}> | |||
| <td> | |||
| {_.name} | |||
| {_.name}{_.unit && (', ' + _.unit)} | |||
| </td> | |||
| <td class="right"> | |||
| <div style="display: inline-block;"> | |||
| @@ -86,20 +86,19 @@ export default defineComponent({ | |||
| type="number" | |||
| /> | |||
| </div> | |||
| <span class="breathe">{_.unit}</span> | |||
| </td> | |||
| </tr> | |||
| ))} | |||
| <tr> | |||
| <td colspan="2"> | |||
| <strong>Compounded (T3 and T4, "Cynoplus")</strong> | |||
| <strong>Compounded (T3 and T4 - "Cynoplus")</strong> | |||
| </td> | |||
| </tr> | |||
| <tr class="ratios"> | |||
| <td> | |||
| Desired Ratio (T3:T4) | |||
| </td> | |||
| <td class="right"> | |||
| <td class="right ratio"> | |||
| <div> | |||
| <input | |||
| value={t3Ratio.value} | |||
| @@ -110,8 +109,8 @@ export default defineComponent({ | |||
| step="1" | |||
| type="number" | |||
| /> | |||
| </div>{" "} | |||
| :{" "} | |||
| </div> | |||
| <span class="separator"/> | |||
| <div> | |||
| <input | |||
| value={t4Ratio.value} | |||
| @@ -127,9 +126,9 @@ export default defineComponent({ | |||
| </tr> | |||
| <tr class="synthetic"> | |||
| <td> | |||
| Synthetic T3/T4 Combo | |||
| T3 mcg : T4 mcg | |||
| </td> | |||
| <td class="right"> | |||
| <td class="right ratio"> | |||
| <div> | |||
| <input | |||
| value={compounded.value.t3Syn} | |||
| @@ -143,8 +142,8 @@ export default defineComponent({ | |||
| step="1" | |||
| type="number" | |||
| /> | |||
| </div>{" "} | |||
| :{" "} | |||
| </div> | |||
| <span class="separator"/> | |||
| <div> | |||
| <input | |||
| value={compounded.value.t4Syn} | |||
| @@ -169,7 +168,7 @@ export default defineComponent({ | |||
| <p> | |||
| <ul> | |||
| <li> | |||
| 1st November 2024: Migrated to new web framework and fixed some buggy input. | |||
| 1st November 2024: Migrated to new web framework and fixed some buggy input/ugly styling. | |||
| </li> | |||
| <li> | |||
| 13th March 2024: Removed the synthetic/pure distinction as it was confusing and | |||
| @@ -64,8 +64,9 @@ export default defineComponent({ | |||
| {rpArticles.value && rpArticles.value.map((_) => ( | |||
| <li> | |||
| <RouterLink to={{ name: "GEDeutschArticle", params: { articleName: _.slug } }}> | |||
| {_.name} | |||
| {_.title} | |||
| </RouterLink> | |||
| {_.tags?.map(tag => <span class="tag">{tag}</span>)} | |||
| </li> | |||
| ))} | |||
| </ul> | |||
| @@ -1,7 +1,9 @@ | |||
| import { createTextVNode, defineComponent, h, inject, onServerPrefetch, ref, type VNode, watchEffect } from "vue"; | |||
| import { createTextVNode, computed, defineComponent, h, inject, onServerPrefetch, ref, type VNode, watchEffect } from "vue"; | |||
| import { RouterLink } from "vue-router"; | |||
| import useAsyncState from "@/useAsyncState.ts"; | |||
| import useHead from "@/useHead.ts"; | |||
| import DJEmail from "@/DJEmail.tsx"; | |||
| import getDJAPI from "@/api.ts"; | |||
| export default defineComponent({ | |||
| name: "ge-deutsch-article", | |||
| @@ -23,25 +25,32 @@ export default defineComponent({ | |||
| (innerHTML: string) => Object.assign(document.createElement("div"), { innerHTML }), | |||
| ); | |||
| const title = ref(""); | |||
| const { result: articleContent, stateIsReady } = useAsyncState( | |||
| "ge-deutsch-article-data", | |||
| async ({ hostUrl }) => { | |||
| const articleResponse = await fetch(`${hostUrl}/generative-energy/content/${props.articleName}.html`); | |||
| const result = await articleResponse.text(); | |||
| title.value = result.split('<h1 lang="de">')[1].split("</h1>")[0]; | |||
| return result; | |||
| }, | |||
| ); | |||
| const { | |||
| result: articleData, | |||
| stateIsReady: articleDataReady, | |||
| } = useAsyncState('article-data', ({hostUrl}) => getDJAPI(hostUrl, '/rp-articles')); | |||
| const articleMetadata = computed(() => articleData.value?.find(_ => _.slug === props.articleName)); | |||
| const title = computed(() => { | |||
| return articleContent.value?.split('<h1 lang="de">')[1].split("</h1>")[0] ?? 'Artikel'; | |||
| }); | |||
| useHead({ title }); | |||
| onServerPrefetch(() => | |||
| new Promise<void>((res) => { | |||
| watchEffect(() => { | |||
| if (title.value !== "") { | |||
| console.log("resolve", title.value); | |||
| res(); | |||
| } | |||
| }); | |||
| @@ -59,7 +68,7 @@ export default defineComponent({ | |||
| children.unshift(h("button", { | |||
| class: "swap", | |||
| onClick: (e) => { | |||
| e.target.parentElement.classList.toggle("swap"); | |||
| (e.target as HTMLButtonElement).parentElement?.classList.toggle("swap"); | |||
| }, | |||
| }, "↻")); | |||
| } | |||
| @@ -71,7 +80,11 @@ export default defineComponent({ | |||
| } | |||
| } | |||
| return h((node as Element).tagName, attrs, children); | |||
| if (el.tagName === "H1") { | |||
| return h("header", attrs, h("h1", {}, children)); | |||
| } else { | |||
| return h((node as Element).tagName, attrs, children); | |||
| } | |||
| } else { | |||
| return createTextVNode(node.textContent ?? ""); | |||
| } | |||
| @@ -85,7 +98,8 @@ export default defineComponent({ | |||
| return <div>Artikel lädt...</div>; | |||
| } | |||
| await stateIsReady; | |||
| await Promise.all([stateIsReady, articleDataReady]); | |||
| console.log(articleMetadata.value); | |||
| return () => ( | |||
| <div class="ge-article"> | |||
| @@ -95,6 +109,12 @@ export default defineComponent({ | |||
| Sprache auf <span>{currentLang.value === "en" ? "Deutsch" : "Englisch"}</span> umschalten | |||
| </button> | |||
| </div> | |||
| <p class="text-slab"> | |||
| Bei dem untenstehenden Artikel handelt es sich um eine hobbymäßige, amateurhafte Übersetzung des | |||
| Artikels „{ title.value }“ von Ray Peat. Bei Ungenauigkeiten oder Fehlübersetzungen freue ich mich über <DJEmail>eine Mail</DJEmail>! | |||
| </p> | |||
| { articleMetadata.value?.tags?.includes('in-arbeit') && <h5 class="baustelle">🚧 Bitte beachte, dass diese Übersetzung noch in Arbeit und darum nicht fertig ist! 🚧</h5> } | |||
| <hr /> | |||
| <article class={`lang-${currentLang.value}`}> | |||
| <ArticleContentTransformed /> | |||
| </article> | |||
| @@ -1,5 +1,6 @@ | |||
| import { RouterLink } from "vue-router"; | |||
| import useHead from "@/useHead.ts"; | |||
| import DJTooltip from "@/DJTooltip.tsx"; | |||
| export default { | |||
| name: "ge-main", | |||
| @@ -20,16 +21,14 @@ export default { | |||
| <h2>Links</h2> | |||
| <ul> | |||
| <li> | |||
| <RouterLink to={{ name: "GECalculator" }}>Thyroid Calculator</RouterLink> | |||
| <span style="display: none" class="tooltip"> | |||
| Convert to and from grains, set ratios, etc. | |||
| </span> | |||
| <DJTooltip tooltip="Convert to and from grains, set ratios, etc."> | |||
| <RouterLink to={{ name: "GECalculator" }}>Thyroid Calculator</RouterLink> | |||
| </DJTooltip> | |||
| </li> | |||
| <li> | |||
| <RouterLink to={{ name: "GEDeutsch" }}>Ray Peat Articles in German</RouterLink> | |||
| <span style="display: none" class="tooltip"> | |||
| A selection of articles by Ray that I have translated in my spare time into German. | |||
| </span> | |||
| <DJTooltip tooltip="A selection of articles by Ray that I have translated in my spare time into German."> | |||
| <RouterLink to={{ name: "GEDeutsch" }}>Ray Peat Articles in German</RouterLink> | |||
| </DJTooltip> | |||
| </li> | |||
| </ul> | |||
| </div> | |||
| @@ -1,4 +1,4 @@ | |||
| import { defineComponent, Suspense, type VNode } from "vue"; | |||
| import { defineComponent, ref, Suspense, type VNode } from "vue"; | |||
| import { type RouteRecordRaw, RouterLink, RouterView, useRoute } from "vue-router"; | |||
| import GEMain from "@/generative-energy/GEMain.tsx"; | |||
| import DJEmail from "@/DJEmail.tsx"; | |||
| @@ -6,6 +6,7 @@ import GEDeutsch from "@/generative-energy/GEDeutsch.tsx"; | |||
| import GEDeutschArticle from "@/generative-energy/GEDeutschArticle.tsx"; | |||
| import GECalculator from "@/generative-energy/GECalculator.tsx"; | |||
| import DJDonate from "@/DJDonate.tsx"; | |||
| import { setupTooltip } from "@/DJTooltip.tsx"; | |||
| export const routes: RouteRecordRaw[] = [ | |||
| { | |||
| @@ -41,8 +42,11 @@ export default defineComponent({ | |||
| name: "ge-root", | |||
| setup() { | |||
| const route = useRoute(); | |||
| const carrier = ref<HTMLDivElement | null>(null); | |||
| setupTooltip({ carrier }); | |||
| return () => ( | |||
| <> | |||
| <div ref={carrier} class="tooltip-carrier" /> | |||
| <main> | |||
| <RouterLink class={"home-btn" + (route.name === "GEMain" ? " hide" : "")} to={{ name: "GEMain" }}> | |||
| Generative Energy Home | |||
| @@ -1,8 +1,10 @@ | |||
| import { createSSRApp } from "vue"; | |||
| import { createRouter, createWebHistory } from "vue-router"; | |||
| import GERoot, { routes } from "@/generative-energy/GERoot.tsx"; | |||
| import { cssRegistry } from "@/util.ts"; | |||
| createSSRApp(GERoot) | |||
| .provide(cssRegistry, new Set()) | |||
| .use(createRouter({ | |||
| routes, | |||
| history: createWebHistory("/generative-energy"), | |||
| @@ -1,4 +1,7 @@ | |||
| import { createSSRApp } from "vue"; | |||
| import DJHomeRoot from "@/home/DJHomeRoot.tsx"; | |||
| import { cssRegistry } from "@/util.ts"; | |||
| createSSRApp(DJHomeRoot).mount("#app-root"); | |||
| createSSRApp(DJHomeRoot) | |||
| .provide(cssRegistry, new Set()) | |||
| .mount("#app-root"); | |||
| @@ -5,6 +5,7 @@ export type DJSSRContext = { | |||
| title: MaybeRefOrGetter<string>; | |||
| }; | |||
| registry: Record<string, unknown>; | |||
| styles: Record<string, string>; | |||
| }; | |||
| export default function useDJSSRContext() { | |||
| @@ -1,3 +1,6 @@ | |||
| import { type InjectionKey, inject } from 'vue'; | |||
| import useDJSSRContext from "@/useDJSSRContext.ts"; | |||
| export function gid(id: string, doc: (Document | ShadowRoot) | undefined) { | |||
| return ((doc ?? document).getElementById(id)); | |||
| } | |||
| @@ -37,9 +40,23 @@ export function css(strs: TemplateStringsArray, ...vals: string[]) { | |||
| for (let i = 1; i < strs.length; i++) { | |||
| result += vals[i] + strs[i]; | |||
| } | |||
| const sheet = new CSSStyleSheet(); | |||
| sheet.replaceSync(result); | |||
| return sheet; | |||
| return result; | |||
| } | |||
| export const cssRegistry = Symbol('css-registry') as InjectionKey<Set<string>>; | |||
| export function addCSS(key: string, css: string) { | |||
| const context = useDJSSRContext(); | |||
| if (context && !context.styles[key]) { | |||
| context.styles[key] = css; | |||
| } else { | |||
| const registry = inject(cssRegistry); | |||
| if (!registry?.has(key)) { | |||
| const stylesheet = new CSSStyleSheet(); | |||
| stylesheet.replace(css); | |||
| document.adoptedStyleSheets.push(stylesheet); | |||
| } | |||
| } | |||
| } | |||
| /* | |||
| @@ -13,20 +13,23 @@ const utf8Decoder = new TextDecoder("utf-8"); | |||
| const parser = new DOMParser(); | |||
| function appHeaderScript(params: { appstate: Record<string, unknown>; entryPath: string }) { | |||
| function appHeaderScript(params: { ssrContext: DJSSRContext, entryPath: string }) { | |||
| return `<script type="importmap"> | |||
| { | |||
| "imports": { | |||
| "vue": "/deps/vue/dist/vue.esm-browser.prod.js", | |||
| "vue-router": "/deps/vue-router/dist/vue-router.esm-browser.js", | |||
| "vue/jsx-runtime": "/deps/vue/jsx-runtime/index.mjs", | |||
| "@vue/devtools-api": "/deps/@vue/devtools-api/lib/esm/index.js", | |||
| "@vue/devtools-api": "/app/devtools-shim.ts", | |||
| "@/": "/app/" | |||
| } | |||
| } | |||
| </script> | |||
| <style> | |||
| ${ Object.values(params.ssrContext.styles).join('\n') } | |||
| </style> | |||
| <script type="module"> | |||
| window.appstate = ${JSON.stringify(params.appstate)}; | |||
| window.appstate = ${JSON.stringify(params.ssrContext.registry)}; | |||
| import('${params.entryPath}'); | |||
| </script>`; | |||
| } | |||
| @@ -47,7 +50,7 @@ for await (const path of publicFiles) { | |||
| sites.push(path); | |||
| } | |||
| function getAPIResponse(apiReq: Request): Response { | |||
| async function getAPIResponse(apiReq: Request): Promise<Response> { | |||
| let jsonResponse: DJAPIResult | { error: string } | null = null; | |||
| let status = 200; | |||
| @@ -62,22 +65,42 @@ function getAPIResponse(apiReq: Request): Response { | |||
| const apiPath = pathname.split("/api")[1]; | |||
| if (apiPath === "/rp-articles") { | |||
| jsonResponse = [ | |||
| { name: "Koffein: ein vitamin-ähnlicher Nährstoff, oder Adaptogen", slug: "caffeine" }, | |||
| { | |||
| name: "TSH, Temperatur, Puls, und andere Indikatoren bei einer Schilddrüsenunterfunktion", | |||
| slug: "hypothyroidism", | |||
| }, | |||
| ] satisfies DJAPIResultMap["/rp-articles"]; | |||
| } | |||
| const paths: string[] = []; | |||
| const contentDir = './public/generative-energy/content/'; | |||
| for await (const dirEnt of Deno.readDir(contentDir)) { | |||
| if (dirEnt.isFile && dirEnt.name.endsWith('.html')) { | |||
| paths.push(`${contentDir}${dirEnt.name}`); | |||
| } | |||
| } | |||
| const result: DJAPIResultMap['/rp-articles'] = []; | |||
| for (const filePath of paths) { | |||
| const content = await Deno.readTextFile(filePath); | |||
| const dom = parser.parseFromString(content, 'text/html'); | |||
| const metadata = { title: '', tags: [] as string[], slug: '' }; | |||
| const metaTags = dom.querySelectorAll('meta') as unknown as NodeListOf<HTMLMetaElement>; | |||
| for (const metaTag of metaTags) { | |||
| const name = metaTag.attributes.getNamedItem('name')?.value ?? ''; | |||
| const content = metaTag.attributes.getNamedItem('content')?.value ?? ''; | |||
| if (name === 'title') { | |||
| metadata.title = content; | |||
| } else if (name === 'tags') { | |||
| metadata.tags = content ? content.split(",") : []; | |||
| } else if (name === 'slug') { | |||
| metadata.slug = content; | |||
| } | |||
| } | |||
| result.push(metadata); | |||
| } | |||
| jsonResponse = result; | |||
| console.log(result); | |||
| } | |||
| } | |||
| if (!jsonResponse) { | |||
| jsonResponse = { error: `API route ${ apiPath } not found.` }; | |||
| status = 404; | |||
| } | |||
| } | |||
| const headers = new Headers(); | |||
| headers.set("Content-Type", "application/json"); | |||
| return new Response(JSON.stringify(jsonResponse), { | |||
| @@ -93,13 +116,17 @@ Deno.serve({ | |||
| console.log(`Listening on port http://${hostname}:${port}/`); | |||
| }, | |||
| }, async (req, _conn) => { | |||
| const timeStart = new Date().getTime(); | |||
| let response: Response | null = null; | |||
| const url = URL.parse(req.url); | |||
| if (req.method === "GET") { | |||
| const pathname = URL.parse(req.url)?.pathname ?? "/"; | |||
| const pathname = url?.pathname ?? "/"; | |||
| if (pathname.startsWith("/api/")) { | |||
| response = getAPIResponse(req); | |||
| response = await getAPIResponse(req); | |||
| } | |||
| // Public/static files | |||
| @@ -144,7 +171,7 @@ Deno.serve({ | |||
| app.provide("dom-parse", (innerHTML: string) => { | |||
| return parser.parseFromString(innerHTML, "text/html").documentElement; | |||
| }); | |||
| const ssrContext: DJSSRContext = { registry: {}, head: { title: "" } }; | |||
| const ssrContext: DJSSRContext = { styles: {}, registry: {}, head: { title: "" } }; | |||
| if (router) { | |||
| await router.replace(pathname.split("/generative-energy")[1]); | |||
| await router.isReady(); | |||
| @@ -155,7 +182,7 @@ Deno.serve({ | |||
| .replaceAll("%TITLE%", toValue(ssrContext.head?.title) ?? "Site") | |||
| .replace( | |||
| `<!-- SSR HEAD OUTLET -->`, | |||
| appHeaderScript({ appstate: ssrContext.registry, entryPath: clientEntry }), | |||
| appHeaderScript({ ssrContext, entryPath: clientEntry }), | |||
| ); | |||
| response = new Response(content, { headers: { "Content-Type": "text/html" } }); | |||
| } | |||
| @@ -164,7 +191,13 @@ Deno.serve({ | |||
| response = new Response("Only GET allowed.", { status: 500 }); | |||
| } | |||
| return response ?? new Response("Not found.", { status: 404 }); | |||
| response ??= new Response("Not found.", { status: 404 }) | |||
| const timeEnd = new Date().getTime(); | |||
| console.log(`Request ${ url?.pathname ?? 'malformed' }\tStatus ${ response.status }, Duration ${ timeEnd - timeStart }ms`); | |||
| return response; | |||
| }); | |||
| Deno.addSignalListener("SIGINT", () => { | |||
| @@ -1,3 +1,7 @@ | |||
| <meta name="slug" content="caffeine"> | |||
| <meta name="title" content="Koffein: ein vitamin-ähnlicher Nährstoff, oder Adaptogen"> | |||
| <meta name="tags" content="in-arbeit"> | |||
| <h1 lang="en">Caffeine: A vitamin-like nutrient, or adaptogen</h1> | |||
| <h1 lang="de">Koffein: ein vitamin-ähnlicher Nährstoff, oder Adaptogen</h1> | |||
| @@ -1,3 +1,7 @@ | |||
| <meta name="title" content="TSH, Temperatur, Puls, und andere Indikatoren bei einer Schilddrüsenunterfunktion"> | |||
| <meta name="slug" content="hypothyroidism"> | |||
| <meta name="tags" content=""> | |||
| <h1 lang="en">TSH, temperature, pulse rate, and other indicators in hypothyroidism</h1> | |||
| <h1 lang="de">TSH, Temperatur, Puls, und andere Indikatoren bei einer Schilddrüsenunterfunktion</h1> | |||
| @@ -14,6 +14,11 @@ h1, h2, h3, h4, h5 { | |||
| font-family: "Roboto Slab", "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; | |||
| } | |||
| .baustelle { | |||
| text-align: center; | |||
| display: block; | |||
| } | |||
| a.home-btn { | |||
| display: block; | |||
| text-align: center; | |||
| @@ -23,6 +28,15 @@ a.home-btn { | |||
| } | |||
| } | |||
| span.tag { | |||
| margin-left: 4px; | |||
| font-style: italic; | |||
| &::before { | |||
| content: '#'; | |||
| } | |||
| } | |||
| .ge-article { | |||
| .header { | |||
| margin-top: 20px; | |||
| @@ -78,28 +92,28 @@ a.home-btn { | |||
| display: none; | |||
| } | |||
| .lang-en *:lang(de) { | |||
| .lang-en span:lang(de) { | |||
| display: none; | |||
| } | |||
| .lang-de *:lang(en) { | |||
| .lang-de span:lang(en) { | |||
| display: none; | |||
| } | |||
| } | |||
| main { | |||
| width: 100%; | |||
| width: calc(100% - 20px); | |||
| margin: auto; | |||
| } | |||
| @media (min-width: 600px) { | |||
| @media (min-width: 900px) { | |||
| main { | |||
| max-width: 1340px; | |||
| margin: auto; | |||
| max-width: 943px; | |||
| min-width: 600px; | |||
| width: 60%; | |||
| } | |||
| } | |||
| header { | |||
| margin: auto; | |||
| text-align: center; | |||
| @@ -174,22 +188,45 @@ footer { | |||
| input { | |||
| width: 70px; | |||
| } | |||
| tr:last-of-type td { | |||
| border-bottom: none; | |||
| } | |||
| td { | |||
| border-bottom: 1px solid var(--text-color); | |||
| border-right: 1px solid var(--text-color); | |||
| padding: 10px; | |||
| } | |||
| td:last-of-type { | |||
| border-right: none; | |||
| } | |||
| td.right { | |||
| text-align: center; | |||
| } | |||
| .breathe { | |||
| padding-left: 4px; | |||
| padding-right: 4px; | |||
| } | |||
| .separator::before { | |||
| content: '—'; | |||
| } | |||
| @media (min-width: 600px) { | |||
| .separator::before { | |||
| content: ':'; | |||
| padding-left: 4px; | |||
| padding-right: 4px; | |||
| } | |||
| td.ratio { | |||
| text-align: left; | |||
| } | |||
| td.right div { | |||
| display: inline-block; | |||
| } | |||
| @@ -183,35 +183,3 @@ a { | |||
| text-decoration: none; | |||
| cursor: pointer; | |||
| } | |||
| .tooltip-container { | |||
| display: inline-block; | |||
| } | |||
| .tooltip-carrier { | |||
| opacity: 0; | |||
| display: block; | |||
| pointer-events: none; | |||
| background-color: black; | |||
| border: white solid 1px; | |||
| color: white; | |||
| padding: 10px; | |||
| position: absolute; | |||
| z-index: 1; | |||
| overflow: hidden; | |||
| height: 0; | |||
| width: 0; | |||
| position: absolute; | |||
| transition: opacity 200ms, height 200ms, width 200ms; | |||
| .text-carrier { | |||
| position: absolute; | |||
| width: 350px; | |||
| font-size: 16px; | |||
| font-family: "Roboto", serif; | |||
| display: block; | |||
| overflow: hidden; | |||
| } | |||
| } | |||