Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { defineConfig } from 'astro/config';

import starlight from '@astrojs/starlight';
import rehypeExternalLinks from 'rehype-external-links';
import rehypeTableScroll from './src/lib/rehype-table-scroll.mjs';
import mdx from '@astrojs/mdx';

const SITE = 'https://stackyard.sandobserver.com';
Expand All @@ -12,6 +13,7 @@ export default defineConfig({
markdown: {
rehypePlugins: [
[rehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'] }],
rehypeTableScroll,
],
},
integrations: [
Expand Down Expand Up @@ -46,6 +48,17 @@ export default defineConfig({
{ tag: 'meta', attrs: { property: 'og:site_name', content: 'Stackyard' } },
{ tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' } },
{ tag: 'meta', attrs: { name: 'twitter:image', content: `${SITE}/img/og.jpg` } },
{
tag: 'script',
content:
"addEventListener('DOMContentLoaded',function(){" +
"var boxes=document.querySelectorAll('.sy-tscroll');" +
"function sync(){boxes.forEach(function(b){" +
"if(b.scrollWidth>b.clientWidth+1){b.tabIndex=0;b.setAttribute('role','group');" +
"b.setAttribute('aria-label','Scrollable table');}" +
"else{b.removeAttribute('tabindex');b.removeAttribute('role');b.removeAttribute('aria-label');}});}" +
"sync();addEventListener('resize',sync);});",
},
],

expressiveCode: {
Expand Down
28 changes: 28 additions & 0 deletions src/lib/rehype-table-scroll.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// @ts-check

/* Wraps every markdown table in a horizontally scrollable container, so a wide
table scrolls instead of clipping its last column on a narrow screen. */

const CLASS = 'sy-tscroll';

/** @param {any} node */
function wrapTables(node) {
if (!node || !Array.isArray(node.children)) return;
const children = node.children;
for (const child of children) wrapTables(child);
node.children = children.map(child =>
child.type === 'element' && child.tagName === 'table'
? {
type: 'element',
tagName: 'div',
properties: { className: [CLASS] },
children: [child],
}
: child,
);
}

export default function rehypeTableScroll() {
/** @param {any} tree */
return tree => wrapTables(tree);
}
72 changes: 68 additions & 4 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,11 @@ const schema = {
<div class="fvis marquee" role="img" aria-label="Services Stackyard widgets can read from">
<div class="mrow mrow-a">
<span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-plex.svg" alt=""><span class="mname">Plex</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-jellyfin.svg" alt=""><span class="mname">Jellyfin</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-emby.svg" alt=""><span class="mname">Emby</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-navidrome.svg" alt=""><span class="mname">Navidrome</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-audiobookshelf.svg" alt=""><span class="mname">Audiobookshelf</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-komga.svg" alt=""><span class="mname">Komga</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-kavita.svg" alt=""><span class="mname">Kavita</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-github.svg" alt=""><span class="mname">GitHub</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-adguard-home.svg" alt=""><span class="mname">AdGuard Home</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-pi-hole.svg" alt=""><span class="mname">Pi-hole</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-technitium.svg" alt=""><span class="mname">Technitium</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-nextdns.svg" alt=""><span class="mname">NextDNS</span></span>
<span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-plex.svg" alt=""><span class="mname">Plex</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-jellyfin.svg" alt=""><span class="mname">Jellyfin</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-emby.svg" alt=""><span class="mname">Emby</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-navidrome.svg" alt=""><span class="mname">Navidrome</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-audiobookshelf.svg" alt=""><span class="mname">Audiobookshelf</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-komga.svg" alt=""><span class="mname">Komga</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-kavita.svg" alt=""><span class="mname">Kavita</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-github.svg" alt=""><span class="mname">GitHub</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-adguard-home.svg" alt=""><span class="mname">AdGuard Home</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-pi-hole.svg" alt=""><span class="mname">Pi-hole</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-technitium.svg" alt=""><span class="mname">Technitium</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-nextdns.svg" alt=""><span class="mname">NextDNS</span></span>
<span class="mdup" aria-hidden="true"><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-plex.svg" alt=""><span class="mname">Plex</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-jellyfin.svg" alt=""><span class="mname">Jellyfin</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-emby.svg" alt=""><span class="mname">Emby</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-navidrome.svg" alt=""><span class="mname">Navidrome</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-audiobookshelf.svg" alt=""><span class="mname">Audiobookshelf</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-komga.svg" alt=""><span class="mname">Komga</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-kavita.svg" alt=""><span class="mname">Kavita</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-github.svg" alt=""><span class="mname">GitHub</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-adguard-home.svg" alt=""><span class="mname">AdGuard Home</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-pi-hole.svg" alt=""><span class="mname">Pi-hole</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-technitium.svg" alt=""><span class="mname">Technitium</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-nextdns.svg" alt=""><span class="mname">NextDNS</span></span></span>
</div>
<div class="mrow mrow-b">
<span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-glances.svg" alt=""><span class="mname">Glances</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-beszel.svg" alt=""><span class="mname">Beszel</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-unraid.svg" alt=""><span class="mname">Unraid</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-scrutiny.svg" alt=""><span class="mname">Scrutiny</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-truenas-scale.svg" alt=""><span class="mname">TrueNAS</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-duplicati.svg" alt=""><span class="mname">Duplicati</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-kopia.svg" alt=""><span class="mname">Kopia</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-gluetun.svg" alt=""><span class="mname">Gluetun</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-conduit.svg" alt=""><span class="mname">Psiphon Conduit</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-netbird.svg" alt=""><span class="mname">NetBird</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-plausible.svg" alt=""><span class="mname">Plausible</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-umami.svg" alt=""><span class="mname">Umami</span></span><span class="mitem"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="3.4"/><path d="M9 2.6v1.7M9 13.7v1.7M3.5 9H1.8M16.2 9h-1.7M5.1 5.1 3.9 3.9M14.1 14.1l-1.2-1.2M12.9 5.1l1.2-1.2M3.9 14.1l1.2-1.2"/><path d="M17.5 21.5H8.8a3.9 3.9 0 0 1-.4-7.8 5 5 0 0 1 9.5.9 3.4 3.4 0 0 1-.4 6.9Z"/></svg><span class="mname">Weather</span></span>
<span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-glances.svg" alt=""><span class="mname">Glances</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-beszel.svg" alt=""><span class="mname">Beszel</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-unraid.svg" alt=""><span class="mname">Unraid</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-scrutiny.svg" alt=""><span class="mname">Scrutiny</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-truenas-scale.svg" alt=""><span class="mname">TrueNAS</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-duplicati.svg" alt=""><span class="mname">Duplicati</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-kopia.svg" alt=""><span class="mname">Kopia</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-gluetun.svg" alt=""><span class="mname">Gluetun</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-conduit.svg" alt=""><span class="mname">Psiphon Conduit</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-netbird.svg" alt=""><span class="mname">NetBird</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-plausible.svg" alt=""><span class="mname">Plausible</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-umami.svg" alt=""><span class="mname">Umami</span></span><span class="mitem"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="3.4"/><path d="M9 2.6v1.7M9 13.7v1.7M3.5 9H1.8M16.2 9h-1.7M5.1 5.1 3.9 3.9M14.1 14.1l-1.2-1.2M12.9 5.1l1.2-1.2M3.9 14.1l1.2-1.2"/><path d="M17.5 21.5H8.8a3.9 3.9 0 0 1-.4-7.8 5 5 0 0 1 9.5.9 3.4 3.4 0 0 1-.4 6.9Z"/></svg><span class="mname">Weather</span></span>
<span class="mdup" aria-hidden="true"><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-glances.svg" alt=""><span class="mname">Glances</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-beszel.svg" alt=""><span class="mname">Beszel</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-unraid.svg" alt=""><span class="mname">Unraid</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-scrutiny.svg" alt=""><span class="mname">Scrutiny</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-truenas-scale.svg" alt=""><span class="mname">TrueNAS</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-duplicati.svg" alt=""><span class="mname">Duplicati</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-kopia.svg" alt=""><span class="mname">Kopia</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-gluetun.svg" alt=""><span class="mname">Gluetun</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-conduit.svg" alt=""><span class="mname">Psiphon Conduit</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-netbird.svg" alt=""><span class="mname">NetBird</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-plausible.svg" alt=""><span class="mname">Plausible</span></span><span class="mitem"><img loading="lazy" decoding="async" src="/icons/w-umami.svg" alt=""><span class="mname">Umami</span></span><span class="mitem"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="3.4"/><path d="M9 2.6v1.7M9 13.7v1.7M3.5 9H1.8M16.2 9h-1.7M5.1 5.1 3.9 3.9M14.1 14.1l-1.2-1.2M12.9 5.1l1.2-1.2M3.9 14.1l1.2-1.2"/><path d="M17.5 21.5H8.8a3.9 3.9 0 0 1-.4-7.8 5 5 0 0 1 9.5.9 3.4 3.4 0 0 1-.4 6.9Z"/></svg><span class="mname">Weather</span></span></span>
</div>
</div>
</div>
Expand Down Expand Up @@ -333,6 +333,32 @@ const schema = {
<script is:inline>
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;

/* The phone grid is fluid, the widget documents render on a fixed 170px canvas.
Scale them from the card the grid actually produced. */
const demoScreen = document.querySelector('.screen-inner');
const phoneDemo = matchMedia('(max-width:768px)');
function fitDemoWidgets() {
if (!demoScreen) return;
if (!phoneDemo.matches) {
demoScreen.style.removeProperty('--sc');
demoScreen.style.removeProperty('--ws');
return;
}
const screenW = demoScreen.clientWidth;
if (screenW) demoScreen.style.setProperty('--sc', String(screenW / 393));
const card = demoScreen.querySelector('.wcell');
const w = card ? card.getBoundingClientRect().width : 0;
if (w) demoScreen.style.setProperty('--ws', String(w / 170));
}
fitDemoWidgets();
addEventListener('resize', fitDemoWidgets);
phoneDemo.addEventListener('change', fitDemoWidgets);

if (reduced) {
const strip = document.querySelector('.marquee');
if (strip) strip.tabIndex = 0;
}

if (!reduced) {
const jf = document.getElementById('jellyfinCount');
const sr = document.getElementById('seerrCount');
Expand Down Expand Up @@ -488,6 +514,7 @@ if (!reduced) {
mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent)}
/* Keep the trailing margin. Using gap breaks the -50% marquee loop. */
.mrow{display:flex;width:max-content;gap:0;align-items:center}
.mdup{display:contents}
.mrow + .mrow{margin-top:34px}
.mrow-a{animation:drift-a 58s linear infinite}
.mrow-b{animation:drift-b 66s linear infinite}
Expand Down Expand Up @@ -581,6 +608,31 @@ if (!reduced) {
.cta h2{font-size:30px}
.cta-in{padding:0 28px}
}
/* The app treats this width as a phone, so the demo shows the phone home
screen: four columns, a small widget over a 2x2 block. Sizes derive from
a 393px reference width, as in the app. */
@media (max-width:768px){
/* The app's mobile home screen: four columns, a small widget over a 2x2
block, sizes derived from a 393px reference width. */
.screen-inner{--sc:.9;--sm:calc(18px * var(--sc));--gap:calc(9px * var(--sc));
--lbl:max(9px, calc(9px * var(--sc)));
padding:var(--sm) var(--sm) 0;height:auto}
.dgrid{grid-template-columns:repeat(4,1fr);grid-auto-rows:auto;
column-gap:var(--gap);row-gap:var(--gap);justify-content:stretch}
.cell{width:auto;height:auto;aspect-ratio:1;gap:calc(3px * var(--sc))}
.itile,.folder{width:74%;height:auto;aspect-ratio:1;border-radius:22.5%}
.itile img{width:64%;height:64%}
.folder{gap:4%;padding:13%}
.ilabel{width:100%;font-size:var(--lbl)}
.badge{top:calc(-5px * var(--sc));right:calc(-5px * var(--sc));
min-width:calc(15px * var(--sc));height:calc(15px * var(--sc));
border-radius:calc(7.5px * var(--sc));padding:0 calc(4px * var(--sc));
font-size:calc(9px * var(--sc))}
.wcell,.np{grid-column:span 2;grid-row:span 2;
width:100%;height:auto;aspect-ratio:1;border-radius:18%;transform:none}
/* Set by the page script from the measured card. */
.wframe,.np-frame{transform:scale(var(--ws,.9))}
}
@media (max-width:600px){
.wrap{padding:0 20px}
.nav{padding:18px 0;gap:12px}
Expand All @@ -596,7 +648,6 @@ if (!reduced) {
.device{padding:0 10px}
.bezel{border-radius:18px 18px 0 0;padding:8px 8px 0}
.screen{border-radius:12px 12px 0 0}
.screen-inner{--gs:.48;height:266px}
.features{padding:0}
.feature{padding:34px 0;gap:22px}
.ftext h2{font-size:22px}
Expand All @@ -622,8 +673,21 @@ if (!reduced) {
.foot-r{align-items:flex-start}
}

/* No animation leaves the rows parked with most of the list out of reach.
Drop the duplicate halves and let the reader scroll the strip instead. */
@media (prefers-reduced-motion: reduce){
.mrow-a,.mrow-b{animation:none !important;transform:translateX(-25%)}
.mrow-a,.mrow-b{animation:none !important;transform:none}
.mdup{display:none}
.marquee{overflow-x:auto;overflow-y:hidden;padding:14px 0;
-webkit-mask-image:none;mask-image:none;
overscroll-behavior-x:contain;scrollbar-width:thin}
.marquee:focus-visible{outline:2px solid var(--accent);outline-offset:4px;border-radius:10px}
.mitem{flex-direction:column;width:auto;height:auto;gap:7px;opacity:1;
transition:none;margin-right:22px}
.mitem:hover{transform:none}
.mrow-a .mname,.mrow-b .mname{position:static;top:auto;bottom:auto;left:auto;
transform:none;opacity:1;padding:0;border:0;background:none;
color:var(--a11y-dim);font-weight:500}
}

.skip{position:absolute;left:-9999px;top:0;z-index:50;padding:10px 16px;border-radius:0 0 10px 0;
Expand Down
26 changes: 26 additions & 0 deletions src/styles/docs.css
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,32 @@ button.sy-iconbtn:focus-visible {
overflow-wrap: anywhere;
}

.sy-tscroll {
overflow-x: auto;
max-width: 100%;
}
.sy-tscroll:focus-visible {
outline: 2px solid var(--sy-teal);
outline-offset: 3px;
border-radius: var(--sy-radius-md);
}
@media (max-width: 768px) {
/* Two columns fit a phone. Three do not, and squeezing them drops the last
column's text out of the row. */
.sl-markdown-content .sy-tscroll table:has(thead th:nth-child(3)) {
min-width: 36rem;
}
/* A long path in a value column otherwise sets the column's width and
squeezes the description into a tall, near-empty ribbon. The first column
holds the row's name, so it keeps whole words. */
.sl-markdown-content .sy-tscroll table td:not(:first-child) code {
overflow-wrap: anywhere;
}
.sy-table-wide table {
table-layout: auto;
}
}

@media (prefers-reduced-motion: reduce) {
}

Expand Down