grooovingerA home for Martin Grubinger on the web.https://grooovinger.com/Import Props Types from a Svelte 5 Component in JSDochttps://grooovinger.com/notes/import-props-types-from-svelte5/https://grooovinger.com/notes/import-props-types-from-svelte5/Mon, 09 Mar 2026 00:00:00 GMT<aside class="aside"><strong>Heads-up:</strong> the following code is for Svelte 5. See <a href="https://grooovinger.com/notes/export-jsdoc-type-from-svelte-components">Export JSDoc Types from Svelte 4 Components</a> for a similar note if you use Svelte 4.</aside>
<p>Sometimes in JSDoc, we need to get the type of a Svelte component’s props in a different module. Svelte provides <code>ComponentProps</code> to achieve that goal:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D">// other-module.js</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">/**</span></span>
<span class="line"><span style="color:#6A737D"> * </span><span style="color:#F97583">@import</span><span style="color:#6A737D"> { ComponentProps } from 'svelte'</span></span>
<span class="line"><span style="color:#6A737D"> * </span><span style="color:#F97583">@import</span><span style="color:#6A737D"> ListComponent from 'path/to/List.svelte'</span></span>
<span class="line"><span style="color:#6A737D"> * */</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">/** </span><span style="color:#F97583">@type</span><span style="color:#B392F0"> {ComponentProps<typeof ListComponent>}</span><span style="color:#6A737D"> */</span></span>
<span class="line"><span style="color:#F97583">let</span><span style="color:#E1E4E8"> list </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8"> id: </span><span style="color:#9ECBFF">'awesome-list'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8"> isAwesome: </span><span style="color:#79B8FF">true</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
<p>First, we import <code>ComponentProps</code> type from the <code>svelte</code> package as well as the default export of the Svelte component, in this example <code><List></code> (<code>List.svelte</code>).</p>
<p>Then, to get the type of the props that our <code><List></code> component defines (see below), we can use <code>ComponentProps<typeof ListComponent></code> to extract just the type of the List component’s <code>$props()</code>. Basically, we provide the type of the ListComponent as a generic type variable to receive the <code>$props</code> that <code>List.svelte</code> define.</p>
<p>For completeness, this is how the Svelte component in this example could be typed with JSDoc:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="svelte"><code><span class="line"><span style="color:#E1E4E8"><</span><span style="color:#85E89D">script</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#6A737D"> // List.svelte</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> /**</span></span>
<span class="line"><span style="color:#6A737D"> * </span><span style="color:#F97583">@typedef</span><span style="color:#B392F0"> {Object}</span><span style="color:#B392F0"> Props</span></span>
<span class="line"><span style="color:#6A737D"> * </span><span style="color:#F97583">@property</span><span style="color:#B392F0"> {string}</span><span style="color:#E1E4E8"> id</span><span style="color:#6A737D"> a nice id</span></span>
<span class="line"><span style="color:#6A737D"> * </span><span style="color:#F97583">@property</span><span style="color:#B392F0"> {boolean}</span><span style="color:#E1E4E8"> [isAwesome</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">true]</span><span style="color:#6A737D"> is this list awesome?</span></span>
<span class="line"><span style="color:#6A737D"> */</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> /** </span><span style="color:#F97583">@type</span><span style="color:#B392F0"> {Props}</span><span style="color:#6A737D"> */</span></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> {id, isAwesome </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> true</span><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> $</span><span style="color:#B392F0">props</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#E1E4E8"></</span><span style="color:#85E89D">script</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">...</span></span></code></pre>Refreshed neo:lights:outhttps://grooovinger.com/notes/2026-02-24-refreshed-neo-lights-out/https://grooovinger.com/notes/2026-02-24-refreshed-neo-lights-out/My game neo:lights:out has gotten a little face-lift.Fri, 06 Mar 2026 00:00:00 GMT<p>Years ago, I built <a href="https://grooovinger.com/projects/neolightsout">neo:lights:out</a> as a way to get hands-on experience with <a href="https://svelte.dev/">Svelte</a> (v3) and <a href="https://sapper.svelte.dev/">Sapper</a> (remember Sapper?). A while ago I migrated it from Sapper to SvelteKit version 1. Now, I updated it again – this time to SvelteKit 2 and Svelte 5.</p>
<p>While fiddling around with it, I noticed that I still enjoy playing that game (the reason I built it in the first place). Maybe you do to?<br>
Try it out (it’s free): <a style="color: hsl(335, 68%, 54%); font-weight: 750;" href="https://neolightsout.grooovinger.com">neolightsout.grooovinger.com</a></p>
<p>Stuff I changed:</p>
<ul>
<li>New font: Lexend Variable (<a href="https://www.lexend.com/">https://www.lexend.com/</a>)</li>
<li>Removed all references to Twitter, just use <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share"><code>navigator.share()</code></a> for share buttons instead.</li>
<li>Migrated all components to Svelte 5 with runes syntax</li>
<li>Better hover/focus-visible states</li>
<li>Moved hosting from Netlify to my own Hetzner VPS, CI/CD through <a href="https://coolify.io/">Coolify</a></li>
</ul>
<p>Stuff I <em>didn’t</em> change:</p>
<ul>
<li>still no tracking and analytics (I have no clue about how many people play it)</li>
<li>still only storing level progress in local storage, as simple as it gets</li>
<li>still let’s you skip levels if you can’t find a solution</li>
<li>still no hints that show the next necessary step. This would be really cool to have though.</li>
<li>kept the look, but did some minor color tweaks</li>
</ul>
<p>Let me know if you experience any issues. I’m also happy to 👋 <a href="https://grooovinger.com/#contact">hear from you</a> if you like this game. 😊</p>
<div style="display: flex; place-content: center;max-width: 360px">
<p><img alt="screenshot of neolightsout game" loading="lazy" decoding="async" width="1170" height="1910" src="https://grooovinger.com/_astro/neolightsout.grooovinger.com.CtLoojSu_ZOFuqm.webp"></p>
</div>Define the Theme Color for Safari 26https://grooovinger.com/notes/2026-02-27-safari-26-header-background/https://grooovinger.com/notes/2026-02-27-safari-26-header-background/With theme_color unshipped, what are our options to control the background color of Safari 26 browser UI?Fri, 27 Feb 2026 00:00:00 GMT<h2 id="tldr">tl;dr</h2>
<p>On Safari 26 on MacOS/iOS/iPadOS 26 the theme color for the browser UI is either taken from the site’s body background color <em>or</em> from a position-fixed element at the very top of the page with a background-color if there is one. <code>theme-color</code> meta tag and <code>theme_color</code> manifest member do not affect the browser theme color.</p>
<p>→ <strong>Here’s a <a href="https://grooovinger.com/demos/safari-theme-color.html">demo</a></strong> ←</p>
<hr>
<p>If you are using Safari 26 on MacOS/iOS/iPadOS 26<sup><a href="#footnote-1" role="doc-noteref">1</a></sup> you might have noticed that on some websites, the browser’s chrome (the UI containing the URL bar, back buttons, tabs etc) has a solid background color matching the page colors. My colleague <a href="https://heid.uk/">Marc</a> and I tried to find out how that is defined and embarked on a journey that was longer than expected.</p>
<h2 id="say-good-bye-to-theme-color-">Say good-bye to theme-color 👋</h2>
<p>In the past, the <a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/theme_color">Theme-Color meta tag</a> was used for that, at least in certain scenarios (like iOS fullscreen “PWAs”, or in other browsers like Vivaldi). But, this meta tag was <a href="https://caniuse.com/meta-theme-color">dropped with Safari 26</a>.</p>
<aside class="aside"> Personal sidenote: I <i>think</i> I'm in favor of dropping this meta tag as it was never implemented across the board. I would argue that the <a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/theme_color">`theme_color` manifest member</a> would be a much better place to define it, but then again, what about different values for dark/light mode? <i>sigh…</i></aside>
<h2 id="back-to-safari-26">Back to Safari 26</h2>
<p>So, if <code>theme_color</code> is not what is used on Safari to define the header background color, what is?</p>
<p>I went inspecting a few websites where Safari colorizes the header:</p>
<p><strong><a href="https://www.theverge.com">theverge.com:</a></strong></p>
<p><img alt="screenshot of the verge homepage, Safari shows a solid black header" loading="lazy" decoding="async" width="1042" height="934" src="https://grooovinger.com/_astro/theverge.Be1IJElW_frrTS.webp"></p>
<p><strong><a href="https://piccalil.li">Piccalilli:</a></strong></p>
<p><img alt="screenshot of the piccalilli homepage, Safari shows a solid sand colored header" loading="lazy" decoding="async" width="1046" height="933" src="https://grooovinger.com/_astro/piccalilli.DMCM8h_P_Ze71Sv.webp"></p>
<aside class="aside">It's easy to hate on many of MacOS/iOS 26 design decisions, but boy that looks slick as hell if you ask me.</aside>
<p>I found a similarity: Safari simply uses the <strong>background color</strong> of the <code><body></code> element. You can even change the background color using Safari Dev Tools and see the changes reflected in the browser UI.</p>
<p><img alt="same piccalilli screenshot, this time with red background and red safari" loading="lazy" decoding="async" width="1673" height="1023" src="https://grooovinger.com/_astro/piccalilli-red.CcmnX54Z_Z2sUOO1.webp"></p>
<p>I thought I cracked the case, but alas – I was wrong. As it turns out there are other websites where Safari clearly does not simply use the body background-color. Check out <a href="https://www.ankerbrot.at"><strong>ankerbrot.at</strong></a> for example:</p>
<p><img alt="screenshot of the ankerbrot.at homepage, beige background, red page header and red browser UI" loading="lazy" decoding="async" width="1064" height="963" src="https://grooovinger.com/_astro/ankerbrot.CM7ONGZN_o2fDY.webp"></p>
<p>The <code><body></code> background is beige, the site’s page header is dark red and so is the browser chrome! What is going on?</p>
<h2 id="the-answer-simplified">The answer (simplified)</h2>
<p>After some more digging I think the rules Safari applies for its colorized chrome are:</p>
<ul>
<li>take the page’s <code><body></code> background-color, <strong><em>except when</em></strong></li>
<li>the page has a <code>position: fixed</code> element with a background-color at the very top* of the page</li>
</ul>
<p><em>* in the sense of block-direction start</em></p>
<p>This fixed element <em>must</em> be 100% wide and have a minimum height of <strong>6px</strong> for Safari to accept it as the base color for it’s chrome. <code>Position: sticky</code> does not count.</p>
<h2 id="applying-a-custom-theme-color">Applying a custom theme-color</h2>
<p>Based on these two observations, I tried to trick the browser into appling a custom color even though <a href="https://geizhals.at/">the site I’m working</a> on does not have a fixed header nor a desirable background color for this use-case. I just needed to find a way to hide the 6px-high fixed element for users while Safari still accepts it for it’s chrome color.</p>
<p>After more experimentation, here’s a list of declarations that cause Safari to <strong>not</strong> accept the fixed element to colorize the browser.</p>
<ul>
<li><code>opacity: 0</code></li>
<li><code>visibility: hidden</code></li>
<li><code>display: none</code></li>
<li><code>z-index: -1</code></li>
<li><code>scale: 0</code></li>
<li><code>transform: scaleX(-100%)</code></li>
<li><code>transform: translateX(-100%)</code></li>
<li><code>clip: rect(1px, 1px, 1px, 1px)</code></li>
<li><code>zoom: 0.000001</code></li>
<li><code>translate: 0 0 -1px</code> (as <a href="https://mastodon.social/@anatudor/116148968182350123">suggested by</a> <a href="https://mastodon.social/@anatudor">Ana Tudor</a> – thanks!)</li>
</ul>
<p>I ran out of options to hide an element for users while still have it visually ready for the browser’s magic.</p>
<h3 id="scroll-driven-animations-to-the-rescue">Scroll-driven animations to the rescue</h3>
<p>I did not care if the element is visible on page load/when scrolled to the very top, we can safely hide it below the “real” header using <code>z-index</code>. But as soon as the user scrolls down, we need to hide the element. Turns out, this is easy to achieve with <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll-driven_animations">scroll-driven animations</a>.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="css"><code><span class="line"><span style="color:#B392F0">.custom-theme-color</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF"> position</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">fixed</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#79B8FF"> top</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">0</span><span style="color:#F97583">px</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#79B8FF"> background-color</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">#bada55</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#79B8FF"> height</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">6</span><span style="color:#F97583">px</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#79B8FF"> width</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">100</span><span style="color:#F97583">%</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#79B8FF"> animation-name</span><span style="color:#E1E4E8">: hide-on-scroll;</span></span>
<span class="line"><span style="color:#79B8FF"> animation-timeline</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">scroll</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#79B8FF"> animation-fill-mode</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">both</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#79B8FF"> animation-range-start</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">10</span><span style="color:#F97583">px</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#79B8FF"> animation-range-end</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">11</span><span style="color:#F97583">px</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#79B8FF"> animation-duration</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">0.1</span><span style="color:#F97583">s</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#79B8FF"> z-index</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">1</span><span style="color:#E1E4E8">; </span><span style="color:#6A737D">/* make sure this is lower than the actual header z-index */</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#F97583">@keyframes</span><span style="color:#FFAB70"> hide-on-scroll</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#B392F0"> 0%</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF"> transform</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">translateY</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#B392F0"> 100%</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF"> transform</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">translateY</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">-100</span><span style="color:#F97583">px</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
<blockquote>
<p>Previously, the rule for <code>.custom-theme-color</code> did not include <code>animation-range</code>. <a href="https://bsky.app/profile/bram.us">Bramus</a> <a href="https://bsky.app/profile/bram.us/post/3mg2yjhg27s2s">suggested</a> to add a range for a more concise <code>@keyframes</code> rule – thank you! This way, the theme-color defining element is only visible if the scroll position is within the top 10px (play around with that value depending on your layout). <em>(added on 2026-03-03)</em>.</p>
</blockquote>
<p>With this solution, the <code>.custom-theme-color</code> element is moved out of the viewport as soon as user starts scrolling while Safari still accepts the background color for the primary color of the browser chrome.</p>
<p>Another interesting observation: if you add a <code>border-bottom</code> to the fixed element, Safari will take <em>that</em> color instead of the background of the element. 🤷</p>
<p>→ <strong>Here’s the <a href="https://grooovinger.com/demos/safari-theme-color.html">link to the demo</a> again</strong>, if you want to play around with it (Safari 26 required to see the effect).</p>
<h2 id="disclaimer-and-caveats">Disclaimer and Caveats</h2>
<p>The solution breaks (i.e. no theme color in the browser) if the page is reloaded at a different scroll position than “top”. Maybe an interactive (JS-based) solution would be better suited, but I wanted to solve it with CSS only.</p>
<p>Also, this would need a check if scroll animations are supported – if not, the element should be hidden right away.</p>
<p>We do not use this in production and a possible production-ready solution needs more testing (especially on iOS).</p>
<hr>
<ul>
<li><em>Edited on 2026-02-28: Added translate declaration as suggested by Ana Tudor, see <a href="#applying-a-custom-theme-color">applying-a-custom-theme-color</a>. Added more specific OS versions, as Curtis Wilcox pointed out Safari 26 on MacOS 15 does not apply page colors to it’s chrome, see <a href="#footnote-1">footnote</a>. Added a <a href="#disclaimer-and-caveats">disclaimer</a> about checking for browser support.</em></li>
<li><em>Edited on 2026-03-03: Improved the CSS rule for the <code>.custom-theme-color</code> to include <code>animation-range</code> as suggested by <a href="https://bsky.app/profile/bram.us">Bramus</a>, see <a href="#scroll-driven-animations-to-the-rescue">Scroll-driven animations to the rescue
</a></em></li>
</ul>
<hr>
<footer aria-label="Footnotes">
<ol>
<li id="footnote-1" role="doc-footnote">
<a href="https://c.im/@cwilcox808">Curtis Wilcox</a>
<a href="https://c.im/@cwilcox808/116148887123324392">mentioned</a> that
on MacOS 15 Safari 26 does not use page colors for the chrome background.
</li>
</ol>
</footer>Style sticky elements when they are stuckhttps://grooovinger.com/notes/2026-02-20-container-scroll-state-queries/https://grooovinger.com/notes/2026-02-20-container-scroll-state-queries/It is now possible to detect if a position: sticky element is stuck with CSS.Tue, 24 Feb 2026 00:00:00 GMT<p>It is now possible to detect whether or not a <code>position: sticky</code> element is currently “stuck” using only CSS. 🤩</p>
<h2 id="meet-container-scroll-state-queries">Meet Container Scroll State Queries</h2>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Conditional_rules/Container_scroll-state_queries">Container Scroll State Queries</a> enable CSS to query the state of an element in regards to its scroll behaviour:</p>
<ul>
<li>Is an element scrollable? If so, in which direction is it scrollable? (use <code>scrollable:</code>)</li>
<li>Is a scroll-snapping element currently snapped? (use <code>snapped:</code>)</li>
<li>Is a sticky element currently stuck? (use <code>stuck: </code>)</li>
</ul>
<p>This feature is one type of <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@container">container queries</a>. To enable querying for scroll state, setup a container using <code>container-type: scroll-state;</code> with an optional <code>container-name</code> on the element you want to query.</p>
<p>In a @container query, you can then check for the scroll state of the <em>container</em> element and apply conditional styling to it descenants:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="css"><code><span class="line"><span style="color:#F97583">@container</span><span style="color:#E1E4E8"> my-container-name scroll-state(stuck: top) {</span></span>
<span class="line"><span style="color:#B392F0"> .inner-element</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF"> color</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">red</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
<h2 id="demo">Demo</h2>
<p class="codepen" data-height="400" data-pen-title="Detect Sticky State with only CSS" data-default-tab="html,result" data-slug-hash="MYjgPEE" data-user="mgrubinger" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/mgrubinger/pen/MYjgPEE">
Detect Sticky State with only CSS</a> by Martin Grubinger (<a href="https://codepen.io/mgrubinger">@mgrubinger</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<h2 id="use-case">Use case</h2>
<p>One use case I came across this recently is on the <a href="https://npmx.dev/package/svelte">npmx.dev package page</a>. When you scroll down a bit, the package name stays on top (due to position: sticky) and, to visualize the boundaries between elements, they add a border to the bottom of the package name element. It seems they use JavaScript to achieve that (probably using Intersection Observer, but I haven’t checked), but this would be a perfect use case for a scroll-state container query.</p>
<video controls="" class="blog-video" style="margin-bottom: 3rem;">
<source src="/2026-02-24-npmx-header-sticky.webm" type="video/webm">
</video>
<h2 id="browser-support">Browser Support</h2>
<p>Currently, only Chromium-based browsers (118+) support this feature. See <a href="https://caniuse.com/wf-container-scroll-state-queries">Container scroll-state queries on Can I Use.</a></p>
<p></p>Import Types from other modules in JSDochttps://grooovinger.com/notes/2026-02-12-import-types-from-other-modules/https://grooovinger.com/notes/2026-02-12-import-types-from-other-modules/Forget @typedef for existing types, use @import instead.Mon, 16 Feb 2026 00:00:00 GMT<p>Today I learned about a less verbose way of importing types from a different module when working in a JSDoc codebase.</p>
<p>Instead of using <code>@typedef</code> to locally redeclare a type, you can straight up use <code>@import</code>:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D">// instead of this:</span></span>
<span class="line"><span style="color:#6A737D">/** </span><span style="color:#F97583">@typedef</span><span style="color:#B392F0"> {import("./other-file.js").MyType}</span><span style="color:#B392F0"> MyType</span><span style="color:#6A737D"> */</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">// do this instead:</span></span>
<span class="line"><span style="color:#6A737D">/** </span><span style="color:#F97583">@import</span><span style="color:#6A737D"> { MyType } from "other-file.js" */</span></span></code></pre>
<p>You can then use the <code>MyType</code> type in this module.</p>
<p>I find this syntax much nicer to work with! I also like that the syntax is basically the same as in ESM (except for the <code>@</code> prefix).</p>
<h2 id="disclaimer">Disclaimer</h2>
<p>Even though this is technically not part of JSDoc, it works in VSCode (and probably any other editor that uses TypeScript for type checking).</p>
<h2 id="source">Source</h2>
<p>Source and more extensive example:
<a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-5-beta/#type-imports-in-jsdoc">https://devblogs.microsoft.com/typescript/announcing-typescript-5-5-beta/#type-imports-in-jsdoc</a></p>Editing my Astro site with Astro Editorhttps://grooovinger.com/notes/2026-01-12astro-editor/https://grooovinger.com/notes/2026-01-12astro-editor/Better editing experience for astro sitesTue, 13 Jan 2026 00:00:00 GMT<p>I recently discovered a nice little Mac OS app called <a href="https://astroeditor.danny.is/">Astro Editor</a> by Danny Smith (<a href="https://bsky.app/profile/danny.is">Bluesky</a>).</p>
<p>Just point it to an <a href="https://astro.build/">Astro</a> project, it will read the schemas and then presents a UI with three main parts:</p>
<ul>
<li>a basic navigator to your content collections (left sidebar)</li>
<li>an editor (centered)</li>
<li>frontmatter editing pane (right sidebar)</li>
</ul>
<h2 id="frontmatter-editing">Frontmatter editing</h2>
<p>The main value it adds to my writing experience is the frontmatter editor in the right sidebar. As it knows your content collections, it can render out UI elements depending on the defined type. For example, it will show a date picker for fields of type <code>date</code>.</p>
<h2 id="draft">Draft</h2>
<p>Fields named <code>draft</code> get a special treatment with a red “Draft” badge in the navigator. Very helpful! I added a similar looking element to the list of posts on my website while developing, so that I can spot drafts easily.</p>
<p><img alt="astro-editor-draft.png" loading="lazy" decoding="async" width="810" height="220" src="https://grooovinger.com/_astro/2026-01-12-astro-editor-draft.COOd_kNZ_ZXfIXp.webp"></p>
<h2 id="writing">Writing</h2>
<p>The writing area in the center is – in their own words – inspired by iA Writer. After using it for a while, my verdict is: it’s mostly fine, but could benefit from font-size controls (cmd-plus/cmd-minus) and better contrast. But in general I like it and find mostly enjoyable.</p>
<p>You can even drag and drop images from the filesystem into the editing view and move the file to a defined location, but will always insert it at the cursor – so make sure to place the cursor on the correct spot before inserting an image.</p>
<h2 id="progress">Progress</h2>
<p>It seems Astro Editor is maintained and gets frequent updates, which leaves me hopeful that annoying bugs (like the datepicker being one day off) get resolved soon.</p>
<h2 id="screenshot">Screenshot</h2>
<p>This is what this very post looks like in Astro Editor:</p>
<p><img alt="astro-editor.webp" loading="lazy" decoding="async" width="2940" height="1760" src="https://grooovinger.com/_astro/2026-01-12-astro-editor.B0l_PE1G_Z2bz03A.webp"></p>Run code for incoming emails with Cloudflare Email Workershttps://grooovinger.com/notes/2026-01-12--cloudflare-email-workers/https://grooovinger.com/notes/2026-01-12--cloudflare-email-workers/Mon, 12 Jan 2026 00:00:00 GMT<p>Cloudflare has a feature called <a href="https://developers.cloudflare.com/email-routing/email-workers/">Email Workers</a>, that lets you execute a worker as part of an email workflow. This is pretty niche, but I recently used it to create a waitlist and I think it is both very niche and pretty awesome.</p>
<p>Here’s the gist:</p>
<ul>
<li>User sends an email to a specific email adress, like <a href="mailto:[email protected]">[email protected]</a></li>
<li>The Cloudflare Email Worker which is attached to the email adress gets called</li>
<li>The worker sends a PUT request to my own service to store the email adress in a waitlist DB table (or, use one of Cloudflares own data storage options directly)</li>
<li>the email gets delivered as expected to <a href="mailto:[email protected]">[email protected]</a></li>
</ul>
<p>In my view, sending the email can be considered an active act (and Cloudflare implements checks regarding email sender adress as far as I can tell), this is a simple way to skip the “confirm your email” workflow for something like a waiting list.</p>PWA install experience is top-notch (on desktop)https://grooovinger.com/notes/pwa-install-experience-is-top-notch/https://grooovinger.com/notes/pwa-install-experience-is-top-notch/…when using Chromium-browsers…on Mac OS.Tue, 02 Dec 2025 00:00:00 GMT<p>Hear me out: There is no better install story than the one you get for PWAs (<a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps">progressive web apps</a>) on Mac OS, at least if you’re using a Chromium-based browser.</p>
<p>PWAs have come a long way, and I have been enjoying installing and using PWAs on Mac OS quite a bit lately. I use internal productivity tools, messenger apps, social media apps (like <a href="https://phanpy.social">phanpy</a>) as well as media players like YouTube as PWAs these days.</p>
<p>I’ll take the YouTube progressive web app (<a href="https://youtube.com">https://youtube.com</a>) as an example.</p>
<p>While watching a clip, hit the install button (on the right-hand side in the URL bar), confirm – and <em>boom</em> 💥, you’re done.</p>
<p><img alt="Screenshot of the PWA install button in Chrome" loading="lazy" decoding="async" width="226" height="96" src="https://grooovinger.com/_astro/2025-12-03-pwa-install-button-1.HQJgwa8g_Z13NP1.webp"></p>
<p>From now on, you can open the YouTube app using Spotlight/Raycast, keep it in the dock etc – whatever you do with “regular” apps. Your still on the same video page as before the install, still logged in etc. And – this one blows my mind: <strong>the video even continues to play during install!</strong></p>
<p>Chrome will now show button “Open in app” if the PWA is already installed:</p>
<p><img alt="Screenshot of button labeled In App öffnen" loading="lazy" decoding="async" width="361" height="44" src="https://grooovinger.com/_astro/2025-12-03-pwa-installed.DXY1qv8j_1LpyqC.webp"></p>
<p>I can’t think of any other install process that smooth.</p>
<video controls="" class="blog-video" style="margin-bottom: 3rem;">
<source src="/2025-12-03-pwa-install-flow.webm" type="video/webm">
</video>
<h3 id="a-word-on-vivaldi">A word on Vivaldi</h3>
<p>Vivaldi browser does not expose the PWA icon next to the URL. To install a website as a PWA, right-click on the tab and select “Install [app name]” (or “Open [app name]” if it’s already installed). See <a href="https://help.vivaldi.com/de/desktop/miscellaneous/progressive-web-apps/">Vivaldis help site</a> for more info.</p>
<h3 id="pwa-install-experience-on-mobile">PWA install experience on mobile</h3>
<p>Install experience on mobile has improved, but is not nearly as great as on desktop. I won’t go into great detail here, as this focuses on desktop, but I have hopes that the situation might improve with the <a href="https://blogs.windows.com/msedgedev/2025/11/24/the-web-install-api-is-ready-for-testing/">Web Install API</a> (behind origin trial in Edge). Bruce Lawson also has <a href="https://brucelawson.co.uk/2025/a-first-look-at-the-web-install-api/">an article on the Web Install API</a>, worth reading!</p>Prevent virtual keyboard overlaying content with the Interactive Widget meta taghttps://grooovinger.com/notes/prevent-virtual-keyboard-overlaying-content/https://grooovinger.com/notes/prevent-virtual-keyboard-overlaying-content/Control how the layout adapts when the virtual keyboard is shown.Tue, 23 Sep 2025 14:10:52 GMT<p>The <code>interactive-widget</code> property (as part of the <code>viewport</code> meta tag) can change the way the layout of a website adapts when the virtual (onscreen) keyboard is shown.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Viewport_meta_element#the_effect_of_interactive_ui_widgets">MDN lists three possible values</a>:</p>
<ul>
<li><code>resizes-visual</code></li>
<li><code>resizes-content</code></li>
<li><code>overlays-content</code></li>
</ul>
<p>You might want to play around with the different values to see which one fits your purpose.</p>
<p>In our case, we chose <code>resizes-content</code>. If the keyboard is shown, the remaining layout is adapted to the content viewport instead of obscurred.</p>
<p>You can see the difference here:</p>
<p><a href="https://geizhals.at/keter-store-it-out-max-gartenbox-anthrazit-a1284930.html">Open the product page on Geizhals.at</a> if you want to further inspect the layout.</p>Talk: Svelte at Geizhals (@ Svelte Vienna)https://grooovinger.com/notes/svelte-at-geizhals-talk-svelte-vienna-june-2025/https://grooovinger.com/notes/svelte-at-geizhals-talk-svelte-vienna-june-2025/My talk at the Svelte Vienna meetup on 23rd of June 2025Tue, 24 Jun 2025 10:36:01 GMT<p>Yesterday I had the honor to present my talk “Svelte at Geizhals” at the Svelte Vienna Meetup. This was my first meetup talk ever, and I was a bit nervous – but the hosts made sure I felt welcome and supported me very well! Thank you, Ermin and JYC!</p>
<p>Here is the link to my slides: <a href="https://grooovinger.com/talks/Svelte-at-Geizhals.pdf">Svelte at Geizhals Slides</a></p>
<p>The feedback I received after the talk was very good, and I had a lot of interesting conversations afterwards. I’m happy I could finally contribute back by giving a talk, after visiting the Svelte Vienna meetup many times in the past.</p>
<p><em>edit</em>: You can watch the recording my my talk on youtube: <a href="https://www.youtube.com/watch?v=aJVdfPk9WmI">https://www.youtube.com/watch?v=aJVdfPk9WmI</a></p>
<p>Notes from other talks:</p>
<h2 id="zeeltephp-svelte--php">ZeeltePHP (Svelte + PHP)</h2>
<p>Harry set out to bridge the gap between PHP and SvelteKit and builds <a href="https://github.com/derharry/svelte-zeeltephp">ZeeltePHP</a> for that. Very engaging talk with lots of live coding (props to Harry for that!) – the crowd was also very supportive in spoting unsaved files and other typos along the way 🙂</p>
<h2 id="adding-full-to-your-full-stack-sveltekit-app">Adding full to your full-stack sveltekit app</h2>
<p><a href="https://jyc.dev/blog">Jean-Yves</a> did another great talk about <a href="https://remult.dev">Remult</a>. They just launched v3 and a stunning new landing page and docs. Great work on that, Remult team!</p>
<p>The talk itself left me speachless – I think I deliberatly had to close my mouth during that talk because to not look like this: 😮😮😮</p>
<p>Jean-Yves delivered a super fun and engaging talk about a tool I never really looked into so far.</p>
<p>Remult looks like a very cool project. They really did think about so many use-cases, and made the whole system very flexible and customizable. Strong Meteor.js vibes, but 10× more awesome! The fact that they support so many Meta-Frameworks and database combinations is just 💯</p>
<p>I really felt inspired to try it out on one of my side projects.</p>
<p>Side note: Recently, I was wondering why there are not more frameworks utilizing <a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">SSE (Server-Sent Events)</a> for progressively updating the DOM from the server. And now here’s Remult doing just that with their <a href="https://remult.dev/docs/ref_livequery">LiveQuery feature</a>. Very exciting.</p>
<h2 id="svelte-vienna">Svelte Vienna</h2>
<p>As usual, I had a great time at the Svelte Vienna meetup. The folks who showed up yesterday made me feel welcome as a first-time speaker, with many faces in the audience nodding along as a sign of support. Highly appreciated!</p>
<p>Svelte Vienna also has a new domain now: <a href="https://www.svelte.at">svelte.at</a></p>
<p>Thanks for organizing, and thanks for hosting to <a href="https://www.lean-coders.at">Lean Coders</a> and <a href="https://aots.io/">Ahead of Time Software</a>.</p>How to deploy Fusion RSS Reader on Coolifyhttps://grooovinger.com/notes/how-to-deploy-fusion-rss-reader-on-coolify/https://grooovinger.com/notes/how-to-deploy-fusion-rss-reader-on-coolify/Mon, 17 Mar 2025 14:22:34 GMT<p>Here is how I set up <a href="https://github.com/0x2E/fusion">Fusion RSS reader</a> on my <a href="https://coolify.io/">Coolify</a>:</p>
<p>(This assumes you already have Coolify installed, otherwise refer to the <a href="https://coolify.io/docs/get-started/installation">installation docs</a>)</p>
<ol start="0">
<li>Log on to your Coolify dashboard</li>
<li>Add a project</li>
<li>select production environment</li>
<li>Add new Resource</li>
<li>Select “Docker Compose Empty”</li>
<li>Add Docker Compose config:</li>
</ol>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="yaml"><code><span class="line"><span style="color:#85E89D">version</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'3'</span></span>
<span class="line"><span style="color:#85E89D">services</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D"> fusion</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D"> image</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'rook1e404/fusion:latest'</span></span>
<span class="line"><span style="color:#85E89D"> ports</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#E1E4E8"> - </span><span style="color:#9ECBFF">'127.0.0.1:3000:8080'</span></span>
<span class="line"><span style="color:#85E89D"> environment</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#6A737D"> # Password for the frontend (login)</span></span>
<span class="line"><span style="color:#E1E4E8"> - </span><span style="color:#9ECBFF">PASSWORD=A_STRONG_PASSWORD</span></span>
<span class="line"><span style="color:#6A737D"> # which path should be served on the fully qualified domain name?</span></span>
<span class="line"><span style="color:#E1E4E8"> - </span><span style="color:#9ECBFF">SERVICE_FQDN_FUSION=/</span></span>
<span class="line"><span style="color:#6A737D"> # which port (must map to the services.fusion.ports property)</span></span>
<span class="line"><span style="color:#E1E4E8"> - </span><span style="color:#9ECBFF">SERVICE_FQDN_FUSION_3000</span></span>
<span class="line"><span style="color:#85E89D"> restart</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">unless-stopped</span></span>
<span class="line"><span style="color:#85E89D"> volumes</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#E1E4E8"> - </span><span style="color:#9ECBFF">'./data:/data'</span></span></code></pre>
<ol start="6">
<li>click “Deploy”</li>
<li>When deployed, the Services section should show the generated subdomain <code><appname>.sslip.io</code></li>
</ol>
<p><img alt="Screenshot of Coolify Services showing a redacted link to a .sslip.io subdomain" loading="lazy" decoding="async" width="1830" height="306" src="https://grooovinger.com/_astro/services.DZC2M7QU_2rTxFu.webp"></p>
<p>Et voilà – you’ve got yourself a fusion instance deployed:</p>
<p><img alt="The Login UI of Fusion RSS reader" loading="lazy" decoding="async" width="872" height="634" src="https://grooovinger.com/_astro/login.gJA4fi-7_Z1CGuhX.webp"></p>Add Forgejo custom Gitlens Remote in VS Codehttps://grooovinger.com/notes/add-forgejo-custom-gitlens-remote/https://grooovinger.com/notes/add-forgejo-custom-gitlens-remote/How to link Forgejo web interface to VS Code as a Gitlens remoteTue, 11 Mar 2025 14:22:34 GMT<p><a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens">Gitlens in VS Code</a> can be linked to a <a href="https://forgejo.org/">Forgejo</a> instance. I find this very useful, as it enables:</p>
<ul>
<li>Open a selected line (or lines) of code in VS Code and open it in Forgejo (right-click on the selected code and select “Open on Remote (Web)”)</li>
<li>Open current branch on Forgejo</li>
<li>Open commit on Forgejo</li>
<li>Open repository on Forgejo</li>
</ul>
<p>All these commands are available in the Command Palette (cmd+shift+p) in VS Code.</p>
<p>Add this config to settings.json in VS Code (cmd+shift+p -> <em>Preferences: Open User Settings (JSON)</em>) (replace all <code><PLACEHOLDERS></code> with your Forgejo host details):</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#9ECBFF">"gitlens.remotes"</span><span style="color:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF"> "regex"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"ssh://(forgejo@<HOSTNAME>)/<PATH>/(.+)"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> "type"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"Custom"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> "name"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"Grooovingers Forgejo"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> "urls"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#79B8FF"> "repository"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"https://<URL-TO-FORGEJO>/<ORGANISATION>/${repo}"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> "branches"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"https://<URL-TO-FORGEJO>/<ORGANISATION>/${repo}/branches"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> "branch"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"https://<URL-TO-FORGEJO>/<ORGANISATION>/${repo}/src/branch/${branch}"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> "commit"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"https://<URL-TO-FORGEJO>/<ORGANISATION>/${repo}/commit/${id}"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> "file"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"https://<URL-TO-FORGEJO>/<ORGANISATION>/${repo}/src/branch/main/${file}${line}"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> "fileInBranch"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"https://<URL-TO-FORGEJO>/<ORGANISATION>/${repo}/src/branch/${branch}/${file}${line}"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> "fileInCommit"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"https://<URL-TO-FORGEJO>/<ORGANISATION>/${repo}/src/commit/${id}/${file}${line}"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> "fileLine"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"#L${line}"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> "fileRange"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"#L${start}-L${end}"</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8">]</span></span></code></pre>
<p>More information about the possible configuration fields on <a href="https://help.gitkraken.com/gitlens/gitlens-settings/#remote-provider-integration-settings">Gitkraken GitLens Settings page</a>.</p>Add Space to MacOS dockhttps://grooovinger.com/notes/add-space-to-macos-dock/https://grooovinger.com/notes/add-space-to-macos-dock/Organize MacOS dock with spacesFri, 21 Feb 2025 00:00:00 GMT<p>I keep searching for this, so here is a link on how to add a bit of spacing between apps on your MacOS dock. This helps me to organize the dock into groups.</p>
<p><a href="https://www.idownloadblog.com/2023/06/01/how-to-add-space-to-dock-mac/">“How to add transparent space separators to the Dock on your Mac” on idownloadblog.com</a></p>Svelte 5 Props @typedef with restPropshttps://grooovinger.com/notes/svelte-5-props-typedef-with-restprops/https://grooovinger.com/notes/svelte-5-props-typedef-with-restprops/How to combine restProps with a typed Props objectThu, 12 Dec 2024 16:06:36 GMT<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D"> /**</span></span>
<span class="line"><span style="color:#6A737D"> * </span><span style="color:#F97583">@typedef</span><span style="color:#B392F0"> {Object}</span><span style="color:#B392F0"> Props</span></span>
<span class="line"><span style="color:#6A737D"> * </span><span style="color:#F97583">@property</span><span style="color:#B392F0"> {string}</span><span style="color:#E1E4E8"> name</span></span>
<span class="line"><span style="color:#6A737D"> */</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> /** </span><span style="color:#F97583">@type</span><span style="color:#B392F0"> {Props & Record<string, any>}</span><span style="color:#6A737D"> */</span></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> { name, </span><span style="color:#F97583">...</span><span style="color:#E1E4E8">restProps } </span><span style="color:#F97583">=</span><span style="color:#B392F0"> $props</span><span style="color:#E1E4E8">();</span></span></code></pre>
<p>Most of my Svelte 5 components are typed with a <code>@typdef</code> JSDoc declaration. This seems to be the easiest way to allow restProps (i.e. “everything else”).</p>
<p>Do you have a better solution? Let me know :)</p>Inspired by Good Enoughhttps://grooovinger.com/notes/inspired-by-good-enough/https://grooovinger.com/notes/inspired-by-good-enough/A breath of fresh air in a world of VC-funded website-lookalikes.Thu, 03 Oct 2024 14:22:34 GMT<p>I recently read about <a href="https://pika.page/">Pika</a> blogging platform on <a href="https://mastodon.social/">Mastodon</a> and checked it out. Oh, nice looking website. Nice choice of fonts, clean layout, fantastic image with some unexpected, but relateable copy around Barry. I was intrigued.</p>
<p><img alt="Pika landing page screenshot showing bold copy "Pika is a pretty good blogging platform by the people at Good Enough. It’s pretty and easy and pretty easy. You should try it!"" loading="lazy" decoding="async" width="596" height="800" src="https://grooovinger.com/_astro/da16ec0e-6164-4394-948b-27a8de715eb7.DDwZF3kD_Z19Xj1M.webp"></p>
<p>Then, I even scrolled down all the way to the footer, where it says “Pika is <a href="https://goodenough.us/">Good Enough</a>, and so are you.” – and followed that link. Look at that – more products, more links! Clicky clicky. I was delighted at what I saw.</p>
<blockquote>
<p>When was the last time you clicked around a company website, just because its design language appealed so much to you?</p>
</blockquote>
<p>For me, it felt what browsing in the past felt like: just follow a bunch of links because you’re curious where it might lead to.</p>
<p>It’s hard to pinpoint what exactly it is that speaks to me, but the overall “look and feel” of those websites is just :100:</p>
<p><strong>So refreshing</strong> – great work, <a href="https://goodenough.us/">Good Enough</a>!</p>
<p><em>Note: this is not an ad. I just want to praise the folks who built these websites in an era, where pretty much all websites look alike.</em></p>The single most effective Cypress trick to improve accessibilityhttps://grooovinger.com/notes/the-single-most-effective-cypress-trick-to-improve-accessibility/https://grooovinger.com/notes/the-single-most-effective-cypress-trick-to-improve-accessibility/Find and fix accessibility issues while writing Cypress tests.Mon, 19 Aug 2024 15:05:41 GMT<p><em>(Sorry for the stupid clickbaity title 😃)</em></p>
<h2 id="tl-dr">tl; dr</h2>
<p>By selecting elements based on their accessible role and name using <a href="https://testing-library.com/docs/cypress-testing-library/intro/">Cypress Testing Library</a>, we effortlessly found accessibility issues while authoring Cypress tests.</p>
<hr>
<p><a href="https://testing-library.com/docs/cypress-testing-library/intro/">Cypress Testing Library</a> is a plugin for <a href="https://www.cypress.io/">Cypress</a> that adds a bunch of additional commands to Cypress. The one I find most effective is <strong><a href="https://testing-library.com/docs/queries/byrole"><code>findByRole</code></a></strong>.</p>
<p>By using <code>findByRole()</code> as much as possible (versus selecting elements based on <code>class</code>, <code>id</code> or <code>data-test-id</code>), we caught a ton of accessibility issues while authoring tests.</p>
<p>We found some elements that had a wrong <strong>role</strong>, like interactive elements that had non-interactive roles (think: divs as buttons etc).</p>
<p>We also found many elements where the accessible <strong>name</strong> of an element did not describe what sighted users would see. An example would be a “close” button that also included an icon name in the accessible name.</p>
<p>As an additional benefit, you get to write tests by thinking about how your users would interact with the page. An example would be:</p>
<p><em>“click on the button that reads ‘login’”</em>
vs
<em>“click on the button with the test-id <code>data-test-id="login"</code>”</em></p>
<p>This way, if classes or ids change (and they will!) your cypress tests will no longer break. But if the text content of an element is changed, your test <em>should</em> and <em>will</em> fail.</p>
<p>Overall this helped us write more natural tests while at the same time catching accessibility issues along the way. Highly recommended.</p>
<blockquote>
<p><strong>Side note:</strong> if you use <a href="https://playwright.dev/">Playwright</a> instead of Cypress, you can use <a href="https://playwright.dev/docs/locators#locate-by-role">getByRole</a> which does the same thing.</p>
</blockquote>Easy screen orientation management with the ScreenOrientation APIhttps://grooovinger.com/notes/easy-screen-orientation-management-with-the-screenorientation-api/https://grooovinger.com/notes/easy-screen-orientation-management-with-the-screenorientation-api/No more matchMedia to check screen orientationTue, 13 Aug 2024 09:06:50 GMT<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation">ScreenOrientation API</a> recently reached Baseline support. This is a very handy API to check for the current orientation of the users device as well as reacting to changing orientation.</p>
<h2 id="interface">Interface</h2>
<h3 id="type">Type</h3>
<p><code>screen.orientation.type</code></p>
<p>lets you read the current device orientation (one of: <code>portrait-primary</code>, <code>portrait-secondary</code>, <code>landscape-primary</code>, <code>landscape-secondary</code>)</p>
<h3 id="angle">Angle</h3>
<p><code>screen.orientation.angle</code></p>
<p>lets you read the current device angle in degrees (0-360)</p>
<h3 id="change-event">Change event</h3>
<p><code>change</code></p>
<p>event to react to change in orientation, e.g. <code>screen.orientation.addEventListener('change', handleOrientationChange)</code></p>
<h3 id="locks">Locks</h3>
<p>ScreenOrientationAPI also lets you request on <a href="https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/lock">orientation lock</a>. This could be useful for games and other experience requiring a stable orientation.</p>
<h2 id="browser-support">Browser support</h2>
<p>Chromium-based browsers supported this API for ages, while <a href="https://caniuse.com/screen-orientation">Safari recently added support in v16.4</a>. So make sure to check for <code>screen.orientation</code> before using this API.</p>
<h2 id="further-reading">Further reading</h2>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/Managing_screen_orientation">Managing screen orientation on MDN</a></p>
<p><a href="https://caniuse.com/screen-orientation">Can I Use: Screen Orientation</a></p>Create a bruno collection from an Open API documenthttps://grooovinger.com/notes/create-a-bruno-collection-from-an-open-api-document/https://grooovinger.com/notes/create-a-bruno-collection-from-an-open-api-document/Using an intermediate Postman collection, we can create a bruno collection based on an Open API documentFri, 12 Jul 2024 12:13:38 GMT<blockquote>
<p><em>edit 11.06.2025</em><br>
Bruno now <a href="https://docs.usebruno.com/open-api/importOAS">supports importing Open API Spec files</a>, at least for Open API v3 files.<br>
Consider the rest of this article obsolete, except when you need to import an OAS v2 (swagger) file.</p>
</blockquote>
<hr>
<p><a href="https://www.usebruno.com/">Bruno</a> does not yet support creating a collection based on an Open API document directly. One way to accomplish that is through an intermediate Postman collection.</p>
<ol>
<li>Log into Postman</li>
<li>In your workspace, select “Import” and upload your Open API Specification</li>
<li>Once imported, choose “Export” from the menu (three dots) next to the imported collection</li>
<li>save the postman collection</li>
<li>Follow the steps in <a href="https://docs.usebruno.com/get-started/import-export-data/import-collections">Import from Postman</a> to import that Postman collection to bruno.</li>
</ol>
<p>Voilà, you have all the endpoints from the Open API specification in bruno.</p>Multiple popovers with Popover APIhttps://grooovinger.com/notes/multiple-popovers-with-popover-api/https://grooovinger.com/notes/multiple-popovers-with-popover-api/use popover=manual to show multiple popovers at a timeTue, 28 May 2024 09:05:14 GMT<p>If you set <a href="https://developer.mozilla.org/en-US/docs/Web/API/Popover_API/Using#using_manual_popover_state"><code>popover=manual</code></a> attribute on the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Popover_API">popover</a> trigger element (like the button that opens a popover), you can have multiple popover elements open at the same time.</p>
<p>Demo/Codepen:</p>
<iframe height="300" style="width: 100%;" scrolling="no" title="CSS Popover API: multiple popovers" src="https://codepen.io/mgrubinger/embed/preview/wvbBOoK?default-tab=&editable=true" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">
See the Pen <a href="https://codepen.io/mgrubinger/pen/wvbBOoK">
CSS Popover API: multiple popovers</a> by Martin Grubinger (<a href="https://codepen.io/mgrubinger">@mgrubinger</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe>Quickly serve a html document via httpshttps://grooovinger.com/notes/quickly-serve-a-html-document-via-https/https://grooovinger.com/notes/quickly-serve-a-html-document-via-https/Thu, 29 Feb 2024 10:16:17 GMT<p>I just needed to quickly serve a simple html document on a remote machine.</p>
<p>Usually I would go for <code>npx serve</code> in the directory where the .html file resides. But <a href="https://www.npmjs.com/package/serve">serve</a> does not support https, so I found this fork of vercels <code>serve</code> package: <a href="https://github.com/warren-bank/node-serve">https://github.com/warren-bank/node-serve</a></p>
<p>Just hit <code>npx @warren-bank/serve --ssl</code></p>
<p>If no certificate is provided, you’ll need to do the “this page is insecure but I still want to see it” dance in your browser, but that was enough for my use case (just needed a “secure context” to use secure browser APIs)</p>
<blockquote>
<p>Optionally, you can provide a real certificate/key/passphrase <a href="https://github.com/warren-bank/node-serve/blob/master/lib/serve/README.md#usage-by-example">as described in the docs</a>.</p>
</blockquote>Automatic resizing of textareas and inputs using CSShttps://grooovinger.com/notes/automatic-resizing-of-textareas-and-inputs-using-css/https://grooovinger.com/notes/automatic-resizing-of-textareas-and-inputs-using-css/field-sizing: content lands in Chrome 123Mon, 19 Feb 2024 17:02:02 GMT<p>There’s a new CSS property landing in Chrome 123 that will automatically resize textareas and inputs based on its content. Since I could not remember its name and only found it by digging through my Mastodon conversations and finding this <a href="https://mastodon.social/@[email protected]/111619407998095697">post by Jen Simmons</a> again, I thought I’d note it down here.</p>
<p>The property is called <code>field-sizing</code> and has two possible values: <code>strict</code> (default, just as before) and <code>content</code> which will automatically grow the element if needed.</p>
<p>The team at <a href="https://polypane.app/">Polypane</a> did a great job explaining it in more detail in <a href="https://polypane.app/blog/field-sizing-just-works">this blog post: Field-sizing just works!</a> – go read it if you have not heard about it.</p>Export JSDoc Types from Svelte 4 Componentshttps://grooovinger.com/notes/export-jsdoc-type-from-svelte-components/https://grooovinger.com/notes/export-jsdoc-type-from-svelte-components/How to export/import JSDoc @typedef definitions in SvelteFri, 09 Feb 2024 12:11:20 GMT<aside class="aside"><strong>Heads-up:</strong> this works for Svelte 4, but currently not in Svelte 5! See this <a href="https://github.com/sveltejs/language-tools/issues/2555">issue in the svelte language server repo</a>. If you only need to export the prop types, this post might help as a workaround: <a href="https://grooovinger.com/notes/import-props-types-from-svelte5">Import Props Types from a Svelte 5 Component in JSDoc</a>.</aside>
<p>The component which <strong>defines the type</strong> needs to do so in a <code><script context="module"></code> block (see <a href="https://svelte.dev/docs/svelte-components#script-context-module">Svelte docs</a> on context=“module”)</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D">// ReusableComponent.svelte</span></span>
<span class="line"><span style="color:#E1E4E8"><</span><span style="color:#85E89D">script</span><span style="color:#B392F0"> context</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"module"</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">/**</span></span>
<span class="line"><span style="color:#E1E4E8"> * An option object for <</span><span style="color:#79B8FF">Select</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8"> * @typedef {Object} SelectOption</span></span>
<span class="line"><span style="color:#E1E4E8"> * @property {string} value the value of this option</span></span>
<span class="line"><span style="color:#E1E4E8"> * @property {string} label the visible label to render</span></span>
<span class="line"><span style="color:#E1E4E8"> */</span></span>
<span class="line"><span style="color:#E1E4E8"></</span><span style="color:#85E89D">script</span><span style="color:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">// ... rest of the component</span></span></code></pre>
<p>then, in the <strong>importing component</strong> you can refer to the type by importing it from the module:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D">// MyComponent.svelte</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8"><</span><span style="color:#85E89D">script</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">import ReusableComponent from './ReusableComponent.svelte`</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">/** @type {</span><span style="color:#F97583">import</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'./ReusableComponent.svelte'</span><span style="color:#E1E4E8">).SelectOption[]} */</span></span>
<span class="line"><span style="color:#E1E4E8">let optionsList = [</span></span>
<span class="line"><span style="color:#E1E4E8"> {label: </span><span style="color:#9ECBFF">'Option 1'</span><span style="color:#E1E4E8">, value: </span><span style="color:#79B8FF">1</span><span style="color:#E1E4E8">},</span></span>
<span class="line"><span style="color:#E1E4E8"> {label: </span><span style="color:#9ECBFF">'Option 2'</span><span style="color:#E1E4E8">, value: </span><span style="color:#79B8FF">2</span><span style="color:#E1E4E8">},</span></span>
<span class="line"><span style="color:#E1E4E8">]</span></span>
<span class="line"><span style="color:#E1E4E8"></</span><span style="color:#85E89D">script</span><span style="color:#E1E4E8">></span></span></code></pre>CSS media query to check for JavaScript supporthttps://grooovinger.com/notes/css-media-query-to-check-for-javascript-support/https://grooovinger.com/notes/css-media-query-to-check-for-javascript-support/Use CSS to conditional styles depending if JavaScript is enabledTue, 02 Jan 2024 11:48:30 GMT<p>You can now use CSS to check if JavaScript is supported using the new <code>Scripting</code> media query.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="css"><code><span class="line"><span style="color:#F97583">@media</span><span style="color:#E1E4E8"> (scripting: none) {</span></span>
<span class="line"><span style="color:#B392F0"> .show-for-noscript-users</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF"> display</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">block</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
<p>This example media query would show elements with the class <code>.show-for-noscript-users</code> if JavaScript is <strong>disabled</strong> in the users browser.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="css"><code><span class="line"><span style="color:#F97583">@media</span><span style="color:#E1E4E8"> (scripting: enabled) {</span></span>
<span class="line"><span style="color:#B392F0"> .show-for-script-users</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF"> display</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">block</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
<p>This example media query would show elements with the class <code>.show-for-script-users</code> if JavaScript is <strong>enabled</strong> in the users browser.</p>
<h2 id="possible-values">Possible Values</h2>
<p>Possible values are:</p>
<ul>
<li><code>enabled</code> – JavaScript is <strong>enabled</strong></li>
<li><code>none</code> – JavaScript is <strong>not enabled</strong></li>
<li><code>initial-only</code> – JavaScript is only enabled during the initial page load, not afterwards.</li>
</ul>
<h3 id="initial-only">Initial Only?</h3>
<p>This one is weird. It means that JavaScript is active during page load, but not afterwards. When would that happen? I can’t think of a case, but <a href="https://chromestatus.com/feature/5075009105559552">Chrome Platform Status</a> mentions that this can <strong>never happen in a browser environment</strong>. <a href="https://github.com/w3c/csswg-drafts/issues/8621">This issue</a> mentions <em>printing</em> environments. My conclusion is: good to know it’s there, probably never going to use it.</p>
<h2 id="browser-support">Browser support</h2>
<p>Support is pretty decent as of January 2023, with Samsung Internet being the only major browser to not support it.</p>
<p>What happens if the browser does not recognize the media query? It will skip the whole block (query), so keep that in mind when using it.</p>
<h2 id="use-cases">Use cases</h2>
<h3 id="get-rid-of-noscript-elements-in-a-flexgrid-layout">Get rid of noscript elements in a flex/grid layout</h3>
<p>Traditionally we would use a combination of <code><noscript></code> and <code><style></code> elements to achieve styling that only applies to noscript environments (JavaScript disabled). However, sometimes an additional <code><noscript></code> element can break flex or grid layouts. Using this media query, we can set the <code>display</code> property of the <code><noscript></code> element to <code>contents</code> to skip it as a direct flex/grid child.</p>
<p><a href="https://codepen.io/mgrubinger/full/dyrGYzL">Demo on CodePen: Grid with a second row of items only for noscript environments</a> (Visit this link with JavaScript disabled to see the second row of the grid)</p>
<h3 id="progressive-enhancement">Progressive Enhancement</h3>
<p>According to <a href="https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement">Progressive Enhancement</a> (PE) approach to building resilient websites, we can use this media query to style things in a certain way if JavaScript is turned off, but <em>enhance</em> the styles with additional rules for when JavaScript it turned <em>on</em> (or vice versa, depending on the use case).</p>
<blockquote>
<p>Keep in mind that there are plenty of situations where JavaScript is <strong>on</strong>, but does not execute, or has not finished executing yet!</p>
</blockquote>
<h3 id="futher-reading">Futher reading</h3>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/scripting">Scripting Media Query on MDN</a></p>
<p><a href="https://caniuse.com/mdn-css_at-rules_media_scripting">Scripting at Can I Use</a></p>Pitch black darkness and the degradation of end user experiencehttps://grooovinger.com/notes/pitch-black-darkness-and-the-degradation-of-end-user-experience/https://grooovinger.com/notes/pitch-black-darkness-and-the-degradation-of-end-user-experience/Thu, 05 Oct 2023 20:49:31 GMT<p>Yesterday I rode my bike home in pitch black darkness, because the bike lamps ran out of battery. Luckily I was able to go almost all the way on backcountry gravel roads and/or cycle paths. No way I would have survived had I ridden on the main road without lights. I felt helpless without those lamps.</p>
<p>I thought: wouldn’t it be great if we had bike lamps powered by the circular motion of the wheels?</p>
<p><strong>Wait, we had that!</strong></p>
<p>When I was a kid*, every bike would be equipped with a dynamo which powered the head- and taillamps of the bike. No worrying about low batteries! No taking the lamps off the bike when parking it in public because otherwise they would be stolen 100%.</p>
<p>Another example: switching on a TV was a matter of pressing one button on a clunky remote. Now? I have to switch on the TV, the receiver, the sound system and set them all to the correct inputs. Luckily I still own a Logitech Harmony, so at least I can do it all through one remote, and some things the Harmony sets up automatically. But this is not the norm, I know many people with two or more remote controls for their TV setup.</p>
<p>This thought lead to the broader question: How come we are accepting such obvious degradations in end-user experience? Sure, the lamps are brighter and the TV sound is from a different league, but still.</p>
<p>Speaking about degradation of end user experience: a big chunk of the “modern” web is barely usable. Ads cluttering the UI, forms failing to submit without proper display of the problem, layout shifts all over the place, text contrast nowhere near conforming WCAG AA levels, janky scrolling experience — I could go on, but you get my point. (Except: have you tried using the facebook mobile website?)</p>
<p>Chris Coyier recently wrote about this topic as well in <a href="https://chriscoyier.net/2023/08/31/a-lot-of-stuff-is-just-fine/">A lot of stuff is just fine</a>. While I generally agree with his article, I do think a lot of things in the “real” world are <em>not</em> fine and could be significantly better.</p>
<blockquote>
<p>What’s extra fricked about all this is that you really gotta try to screw up a website as much as we do. — Chris Coyier in <a href="https://chriscoyier.net/2023/08/31/a-lot-of-stuff-is-just-fine/">A lot of stuff is just fine</a></p>
</blockquote>
<p>Where do we go from here? In my opinion, there are a few things we can all do:</p>
<ul>
<li>buy less shitty things</li>
<li>buy less (in general)</li>
<li>as craftspeople (like web developers): aim to build the best possible product for the <em>end user</em></li>
<li>as product teams: don’t let capitalism ruin your product</li>
<li>as web developers: use the platform; stop using react ffs</li>
</ul>
<hr>
<p>* I know these things are still around, but most bikes are sold without.</p>Svelte Tricks Collectionhttps://grooovinger.com/notes/svelte-tricks-collection/https://grooovinger.com/notes/svelte-tricks-collection/A collection of tricks and niceties you might find useful when working with Svelte.Mon, 08 May 2023 15:50:48 GMT<p>A collection of tricks and niceties you might find useful when working with Svelte. This list is intended to be updated from time to time.</p>
<ol>
<li><a href="#style-components-using-css-variables">Style components using css variables</a></li>
<li><a href="#Forward-events-to-parent-component">Forward events to parent component</a></li>
<li>Locally global styles (with Svelte-Preprocess)</li>
<li>Loop over part of an Array</li>
<li>Dynamic import of components within template</li>
</ol>
<hr>
<h2 id="style-components-using-css-variables">Style components using css variables</h2>
<p>I can think of multiple options to apply external styling for a reusable component:</p>
<ul>
<li>Expose a <code>style</code> prop</li>
<li>Expose a prop for different classes, which you can then apply to the individual elements inside the component</li>
<li>Use the <code>:global()</code> selector in the parent component</li>
<li>Use CSS Custom Properties (CSS vars).</li>
</ul>
<iframe width="100%" height="500px" src="https://stackblitz.com/edit/vitejs-vite-nkl1ru?ctl=1&embed=1&file=src/App.svelte&hideExplorer=1&view=preview"></iframe>
<p>In the example above I used the CSS vars approach. The idea is, that you allow overriding of certain styles based on CSS vars that get applied to the component. Svelte supports setting CSS vars directly when using a component, like this:</p>
<p><code><Component --bg-color="blue" /></code></p>
<blockquote>
<p>Note how you don’t need the style attribute, just pass in the props via <code>--propname</code> attribute!</p>
</blockquote>
<p>Then, in the <code><style></code> block of your reusable component, use the CSS variable – ideally with a fallback value:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#E1E4E8"><</span><span style="color:#85E89D">style</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">.mycomponent {</span></span>
<span class="line"><span style="color:#F97583"> --</span><span style="color:#E1E4E8">bg: </span><span style="color:#B392F0">var</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">--</span><span style="color:#E1E4E8">bg, red);</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#E1E4E8"></</span><span style="color:#85E89D">style</span><span style="color:#E1E4E8">></span></span></code></pre>
<p>I think that’s a clean way to open up styling adjustments for a component. I think of this approach like a separate set of props, just for styling. You might also want to consider using special prefixes for these kind of variables to prevent naming clashes.</p>
<hr>
<h2 id="forward-events-to-parent-component">Forward events to parent component</h2>
<p>If you want a component to pass an event to its parent, all you need to do is to set the attribute on:<eventname> on the element triggering the event. Like this:</eventname></p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#E1E4E8">// within InnerComponent.svelte</span></span>
<span class="line"><span style="color:#E1E4E8"><</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> class</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"card"</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8"> <</span><span style="color:#85E89D">button</span><span style="color:#B392F0"> on:click</span><span style="color:#B392F0"> type</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"button"</span><span style="color:#E1E4E8">>Open</</span><span style="color:#85E89D">button</span><span style="color:#E1E4E8">> // </span><span style="color:#FDAEB7;font-style:italic"><<</span><span style="color:#E1E4E8">-- note the on:click here</span></span>
<span class="line"><span style="color:#E1E4E8"></</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">// within ParentComponent.svelte</span></span>
<span class="line"><span style="color:#E1E4E8"><</span><span style="color:#FDAEB7;font-style:italic">MyComponent</span><span style="color:#B392F0"> on:click</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">{openCard}</span><span style="color:#E1E4E8"> /></span></span></code></pre>
<p>This is not a secret trick, <a href="https://learn.svelte.dev/tutorial/event-forwarding">it’s even in the new official tutorial</a>. I find it very useful and it’s a svelte feature that just makes me authoring components such a joy.</p>
<hr>
<h2 id="locally-global-styles-with-svelte-preprocess">Locally global styles (with Svelte-Preprocess)</h2>
<p><em>(a weird name that I just made up)</em></p>
<p>In Svelte, you can set individual CSS rules to be global using the <code>:global()</code> keyword in the <code>style</code> section. Sometimes you might want to have styles, that are global, but only as long as the element is a child of a certain element to prevent clashes.</p>
<p>Utilizing svelte-preprocess with Less or Sass and postcss you can write rules like this:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#E1E4E8"><</span><span style="color:#85E89D">style</span><span style="color:#B392F0"> lang</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"less"</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#B392F0">.mycomponent</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D"> /* local styles of this component */</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8"> :global {</span></span>
<span class="line"><span style="color:#E1E4E8"> .dynamically-added-element {</span></span>
<span class="line"><span style="color:#79B8FF"> color</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">deeppink</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6A737D"> /* global styles that are only available for elements with class dynamically-added-element which are children of .mycomponent */</span></span>
<span class="line"><span style="color:#E1E4E8"> ...</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#E1E4E8"></</span><span style="color:#85E89D">style</span><span style="color:#E1E4E8">></span></span></code></pre>
<p>Note that the <code>:global {}</code> block is a feature of <a href="https://github.com/sveltejs/svelte-preprocess/blob/main/docs/preprocessing.md#globalstyle">svelte-preprocess</a></p>
<hr>
<h2 id="loop-over-part-of-an-array">Loop over part of an Array</h2>
<p>To run an <code>each</code> loop over just a part of an array, use a dynamically constructed array straight in the template:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#E1E4E8">{#each {</span><span style="color:#B392F0">length</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">5</span><span style="color:#E1E4E8">} </span><span style="color:#F97583">as</span><span style="color:#B392F0"> _</span><span style="color:#E1E4E8">, </span><span style="color:#B392F0">index</span><span style="color:#E1E4E8"> (index)}</span></span>
<span class="line"><span style="color:#E1E4E8"> {@const item </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> items[index] }</span></span>
<span class="line"><span style="color:#F97583"> <!--</span><span style="color:#F97583"> do</span><span style="color:#E1E4E8"> stuff </span><span style="color:#F97583">with</span><span style="color:#E1E4E8"> {item} </span><span style="color:#F97583">--></span></span>
<span class="line"><span style="color:#E1E4E8">{</span><span style="color:#F97583">/</span><span style="color:#E1E4E8">each}</span></span></code></pre>
<p>You might argue that extracting the slice of the array in the <code><script></code> area of a component is more readable. I agree.</p>
<hr>
<h2 id="dynamic-import-of-components-within-template">Dynamic import of components within template</h2>
<p>To dynamically load a component, you can import it straight from the template using <code>#await</code>:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#E1E4E8">{#</span><span style="color:#F97583">await</span><span style="color:#F97583"> import</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"$lib/path/to/ComponentName.svelte"</span><span style="color:#E1E4E8">) }</span></span>
<span class="line"><span style="color:#E1E4E8"> <</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">>loading...</</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">{:then { </span><span style="color:#F97583">default</span><span style="color:#E1E4E8">: ComponentName }}</span></span>
<span class="line"><span style="color:#E1E4E8"> <</span><span style="color:#79B8FF">ComponentName</span><span style="color:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#E1E4E8">{</span><span style="color:#F97583">/await</span><span style="color:#E1E4E8">}</span></span></code></pre>
<p>Line number 5 destructures the imported module to get the <code>default</code> export (our svelte component) and renames it to <code>ComponentName</code>. Be careful not to accidently create request waterfalls by only loading components lazily your application does not need for the initial render.</p>Form with multiple submit buttonshttps://grooovinger.com/notes/form-with-multiple-submit-buttons/https://grooovinger.com/notes/form-with-multiple-submit-buttons/How to handle HTML Forms with multiple submitsThu, 04 May 2023 12:07:52 GMT<h2 id="tldr">tldr;</h2>
<p>A form can have multiple <code>input[type=submit]</code> (or <code><button type=submit></code>) elements. You can specify different values, targets, methods or even different actions for these.</p>
<p>In JavaScript, the information which submit-button was used to submit the form can be accessed in the <a href="https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent/submitter">SubmitEvents <code>submitter</code></a> property.</p>
<h3 id="example">Example:</h3>
<iframe src="https://stackblitz.com/edit/web-platform-cbuerh?embed=1&file=index.html&hideDevTools=1&hideExplorer=1&view=preview" width="100%" height="400px"></iframe>
<hr>
<p>Recently, I’ve been exploring the html <code><form></code> element more, mainly because it’s such a central building block for <a href="https://kit.svelte.dev/">SvelteKit</a> apps. Or, more precisely: forms have been a major building block for web applications forever, but I feel that it’s importance and role has been neglected in the last decade or so of JavaScript-heavy SPAs. Personally, I feel that I have a knowledge gap around such an important building block, and maybe I’m not alone.</p>
<p>The example above has two sections:</p>
<ol>
<li>Form with two submit buttons with the same <code>name</code> attribute, but different values. By default, hitting one of the two submits will submit the form and change the URL of the example website, depending on the value of the clicked element. Kinda like a radio group, but for distinct actions a user can take.</li>
<li>The same form, but this time intercepting the forms <code>submit</code> event and updating text on the website via JavaScript – again, depending on which button was used.</li>
</ol>
<p>There are more things you can specify on a submit element, like the <code>action</code> to submit to or the forms <code>method</code>. See this <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/submit#additional_attributes">MDN page on the input[type=submit]</a> element.</p>Editing Database in VS Code via SQLTools Plugin https://grooovinger.com/notes/editing-database-in-vs-code-via-sqltools-plugin/https://grooovinger.com/notes/editing-database-in-vs-code-via-sqltools-plugin/Stay in the editor while working on your database.Thu, 06 Apr 2023 07:53:07 GMT<p>Lately I’ve been using the plugin <a href="https://marketplace.visualstudio.com/items?itemName=mtxr.sqltools">SQLTools</a> for VS Code for viewing and editing SQL databases.</p>
<p>It’s easy to set up (don’t forget to install the required drivers for your database engine, see the description of <a href="https://marketplace.visualstudio.com/items?itemName=mtxr.sqltools">SQLTools</a>) and easy to use.</p>
<p>I have not yet figured out, why it sometimes would open the result of a query in a new editor pane. Besides that, I find it comfortable to be able to quickly view and edit the database right from my editor.</p>
<p>By the way, this extension also works if you’re working on a remote environment via <a href="https://code.visualstudio.com/docs/remote/ssh">VS Codes Remote-SSH tools</a>.</p>CSS Background Position Offsetshttps://grooovinger.com/notes/css-background-position-offsets/https://grooovinger.com/notes/css-background-position-offsets/Improved positioning of CSS background imagesTue, 14 Mar 2023 07:55:42 GMT<p>You can set an offset for CSS <code>background-position</code> to specify the position of a background image from right and/or bottom boundaries, like this:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="css"><code><span class="line"><span style="color:#B392F0">.my-element</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF"> background-image</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">url</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'path/to/image.png'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#79B8FF"> background-position</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">right</span><span style="color:#79B8FF"> 10</span><span style="color:#F97583">px</span><span style="color:#79B8FF"> bottom</span><span style="color:#79B8FF"> 5</span><span style="color:#F97583">px</span><span style="color:#E1E4E8">;</span></span></code></pre>
<p>This will place the image in the bottom-right corner, but move it 10px to the left and 5px towards the top. Kinda like setting a <code>transform: translateX(-10px, -5px)</code> (which you can’t for background images).</p>
<p>So far I’ve only ever used <code>right bottom</code> or a percentage/pixel based position for background images, but background offset comes in handy when making sure a flush right or bottom background-icon is positioned exactly where you need it.</p>
<p>Also see <a href="https://caniuse.com/css-background-offsets">css-background-offsets on caniuse</a></p>Submit forms in dialogshttps://grooovinger.com/notes/submit-forms-in-dialogs/https://grooovinger.com/notes/submit-forms-in-dialogs/using method=dialogMon, 20 Feb 2023 19:59:58 GMT<p>If you’re using a <code><form></code> element inside a <code><dialog></code> element, you might want to consider setting its <code>method</code> attribute to <code>dialog</code>.</p>
<p>This will cause the dialog to close automatically on form submission and set the value of the submit button to <code>dialog.returnValue</code>. It will not actually submit the form though.</p>
<p>More details:</p>
<ul>
<li><a href="https://webkit.org/blog/12209/introducing-the-dialog-element/">https://webkit.org/blog/12209/introducing-the-dialog-element/</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method">https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method</a></li>
</ul>
<p>I learned about this from <a href="https://reitzner.at/">Dominik Reitzner</a> at the <a href="https://austria.sveltesociety.dev/chapters">Svelte Vienna Meetup</a>.</p>npm: only install production dependencieshttps://grooovinger.com/notes/npm-only-install-production-dependencies/https://grooovinger.com/notes/npm-only-install-production-dependencies/Faster npm ci installsMon, 23 Jan 2023 16:42:15 GMT<p>In a production environment, you should not need node_dependencies from listed in the <code>devDependencies</code> section of your package.json.</p>
<p><code>NODE_ENV=production npm ci</code> will only install dependencies listed in <code>dependencies</code> of your package.json. <code>devDependencies</code> will not be installed.</p>
<p>Alternative: use <code>npm ci --omit=dev</code></p>Sort strings/numbers while respecting special charactershttps://grooovinger.com/notes/sort-stringsnumbers-while-respecting-special-characters/https://grooovinger.com/notes/sort-stringsnumbers-while-respecting-special-characters/using localeCompareTue, 17 Jan 2023 15:53:44 GMT<p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare">localeCompare on MDN</a></p><template> elementhttps://grooovinger.com/notes/template-element/https://grooovinger.com/notes/template-element/... for dynamic contentTue, 17 Jan 2023 15:50:07 GMT<p>The <code><template></code> element is useful if you want to dynamically create markup based on a predefined “template” (great naming right there).</p>
<p>Example markup:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#E1E4E8"><</span><span style="color:#85E89D">template</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8"> <</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8"> <</span><span style="color:#85E89D">h3</span><span style="color:#E1E4E8">>any markup, really</</span><span style="color:#85E89D">h3</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#6A737D"> <!-- watch out for FOUT if you also load relevant styles --></span></span>
<span class="line"><span style="color:#E1E4E8"> <</span><span style="color:#85E89D">link</span><span style="color:#B392F0"> rel</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"stylesheet"</span><span style="color:#B392F0"> href</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"url-to-stylesheet.css"</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8"> </</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8"></</span><span style="color:#85E89D">template</span><span style="color:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8"><</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> class</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"my-target"</span><span style="color:#E1E4E8">></</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span></code></pre>
<p>Example script</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D">// grab the template</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> template</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">getElementById</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'my-template'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#6A737D">// clone the templates content</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> clonedTemplate</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> template.content.firstElementChild.</span><span style="color:#B392F0">cloneNode</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#6A737D">// optionally modify the cloned tree here</span></span>
<span class="line"><span style="color:#6A737D">// append to `.my-target`</span></span>
<span class="line"><span style="color:#E1E4E8">document.</span><span style="color:#B392F0">targetSelector</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'.my-target'</span><span style="color:#E1E4E8">).</span><span style="color:#B392F0">appendChild</span><span style="color:#E1E4E8">(clonedTemplate);</span></span></code></pre>
<blockquote>
<p>👉 The first child of <template> (the <div>) is not strictly necessary, but as the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template">MDN article on <template> states, certain events like click do not work if the templates content is cloned directly (template.content.cloneNode(true)) instead of firstElementChild.cloneNode(true).
<p>See: <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template"><template>: The Content Template element on MDN</template></a></p></template></a></div></template></p></blockquote>Group all DOM elements by font-sizehttps://grooovinger.com/notes/group-all-dom-elements-by-font-size/https://grooovinger.com/notes/group-all-dom-elements-by-font-size/Debug the distribution of font-size of DOM elements on the pageTue, 17 Jan 2023 15:47:19 GMT<p>Group all elements on the current page by their font size:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">let</span><span style="color:#E1E4E8"> elementsBySize </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> [];</span></span>
<span class="line"><span style="color:#E1E4E8">[</span><span style="color:#F97583">...</span><span style="color:#B392F0">$$</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'body *'</span><span style="color:#E1E4E8">)].</span><span style="color:#B392F0">forEach</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">el</span><span style="color:#F97583"> =></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> fontSize </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> window.</span><span style="color:#B392F0">getComputedStyle</span><span style="color:#E1E4E8">(el).fontSize;</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">elementsBySize[fontSize]) elementsBySize[fontSize] </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> [];</span></span>
<span class="line"><span style="color:#E1E4E8"> elementsBySize[fontSize].</span><span style="color:#B392F0">push</span><span style="color:#E1E4E8">(el)</span></span>
<span class="line"><span style="color:#E1E4E8">});</span></span></code></pre>
<p><code>elementsBySize</code> now contains an array with key: font size and value of an array containing all the elements with that font size.</p>
<p><em>Note:</em> this only works in the chrome(ium) devtools, since it uses the $$ syntax. You can replace it with <code>document.querySelectorAll('*')</code></p>Mock navigator.sharehttps://grooovinger.com/notes/mock-navigatorshare/https://grooovinger.com/notes/mock-navigatorshare/For testing in non-secure environmentsTue, 17 Jan 2023 15:44:14 GMT<p>Sometimes you can’t test the native <strong>[<a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share">Navigator.share()</a>](<a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share">https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share</a>)</strong> API, e.g. when the site you’re developing is not served via https.</p>
<p>I used this tiny logger to preview what <em>would</em> have been passed to <code>navigator.share()</code></p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">!</span><span style="color:#E1E4E8">navigator.share </span><span style="color:#F97583">&&</span><span style="color:#E1E4E8"> navigator.</span><span style="color:#B392F0">share</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">...</span><span style="color:#FFAB70">args</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#F97583"> new</span><span style="color:#79B8FF"> Promise</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">...</span><span style="color:#E1E4E8">args)</span></span>
<span class="line"><span style="color:#E1E4E8"> , () </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"error"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8"> }})</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>Was the page navigated to using back or forward button?https://grooovinger.com/notes/was-the-page-navigated-to-using-back-or-forward-button/https://grooovinger.com/notes/was-the-page-navigated-to-using-back-or-forward-button/using PerformanceNavigationTiming.typeTue, 17 Jan 2023 15:34:45 GMT<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#E1E4E8">window.performance.</span><span style="color:#B392F0">getEntriesByType</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"navigation"</span><span style="color:#E1E4E8">)[</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">].type </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> "back_forward"</span><span style="color:#E1E4E8">;</span></span></code></pre>
<p>This is how to detect if a page was loaded because the user navigated via back or forward buttons.</p>
<p>Possible values:</p>
<ul>
<li><code>back_forward</code> if the navigation happened because of a back or forward navigation</li>
<li><code>navigate</code> if the navigation happened because of a regular navigation event (like clicking a link).</li>
<li><code>reload</code> after reloading the page</li>
<li><code>prerender</code> if the navigation has happened during a prerender hint</li>
</ul>
<p>See <a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/type">PerformanceNavigationTiming.type on MDN</a></p>Import and Export a JS Module in one linehttps://grooovinger.com/notes/import-and-export-a-js-module-in-one-line/https://grooovinger.com/notes/import-and-export-a-js-module-in-one-line/export { namedImport } from './path/to/module';Tue, 17 Jan 2023 00:00:00 GMT<p>When you need to immediately export a javascript module again (useful for creating index files, sometimes called “barrel” files):</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">export</span><span style="color:#E1E4E8"> { namedImport } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> './path/to/module'</span><span style="color:#E1E4E8">;</span></span></code></pre>Detect if the tab is hidden or visiblehttps://grooovinger.com/notes/detect-if-the-tab-is-hidden-or-visible/https://grooovinger.com/notes/detect-if-the-tab-is-hidden-or-visible/using document.hiddenTue, 10 Jan 2023 16:55:27 GMT<p>Detect if the document is currently <code>hidden</code> (which means, not active. e.g. user currently has a different tab open)</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#E1E4E8">document.hidden</span></span></code></pre>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/hidden">https://developer.mozilla.org/en-US/docs/Web/API/Document/hidden</a></p>Make an element stick to top and bottom!https://grooovinger.com/notes/make-an-element-stick-to-top-and-bottom/https://grooovinger.com/notes/make-an-element-stick-to-top-and-bottom/Useful CSS to make an element stick to the top and bottom of a scroll container!Tue, 13 Sep 2022 15:52:37 GMT<p>It is possible to make an element stick to the top <strong>and</strong> bottom of a scroll container! You could say: “Of course!”, but I guess I was just surprised it <em>actually</em> works.</p>
<p>Here’s the CSS:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="css"><code><span class="line"><span style="color:#B392F0">.i-am-sticky</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF"> position</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">sticky</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#79B8FF"> top</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">; </span><span style="color:#6A737D">/* or any other value if you want some offset */</span></span>
<span class="line"><span style="color:#79B8FF"> bottom</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">; </span><span style="color:#6A737D">/* or any other value if you want some offset */</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
<p>I find this especially useful for a <em>List/Detail</em> two-column layout where items in the right column shows details when item in the left column is selected (think Apple Mail etc).</p>
<p>This way the selected item always stays visible to the user.</p>
<p>(Imagine trying to solve this using JS, urgh.)</p>
<h3 id="demo">Demo:</h3>
<p>Here’s a quick demo:</p>
<p><a href="https://codesandbox.io/embed/sticky-top-and-bottom-5s9ti">https://codesandbox.io/embed/sticky-top-and-bottom-5s9ti</a></p>How to add a Leaflet map to a Gatsby sitehttps://grooovinger.com/notes/how-to-add-a-leaflet-map-to-a-gatsby-site/https://grooovinger.com/notes/how-to-add-a-leaflet-map-to-a-gatsby-site/Let's fix `window is not defined`Tue, 13 Sep 2022 15:51:40 GMT<blockquote>
<p>👉 <strong>Heads-up:</strong> This post is probably outdated, but the main concept remains the same: don’t use <code>window</code> variables if you do server side rendering or static site generation.</p>
</blockquote>
<p>I have been a long time user and fan of <a href="https://leafletjs.com/">Leaflet</a>, a popular mapping library by <a href="https://agafonkin.com/">@mourner</a>. Recently I needed to integrate a rather simple map (with about 20 markers) to a Gatsby site.</p>
<picture> <source srcset="/_astro/210397494-2506568d-051c-44aa-b28b-b672eac62f25.osUL1_3i_1w9Jx5.webp" type="image/webp"> <img src="https://grooovinger.com/_astro/210397494-2506568d-051c-44aa-b28b-b672eac62f25.osUL1_3i_r1fFs.png" alt="Screenshot of a map showing several pins" loading="lazy" decoding="async" width="800" height="304"> </picture>
<p>As it turns out, there’s a wrapper for Leaflet in React out there: <a href="https://react-leaflet.js.org/">react-leaflet</a> (Some crazy naming right there, I know!). So I installed react-leaflet as a dependency and imported it, created a <code><Map></code>, <code><TileLayer></code> as well as a few <code><Markers></code> in my page component. Et voilà, we have a leaflet map up and showing up in my gatsby site in no time (running in <em>development</em> mode)!</p>
<p>I simply commited and pushed my changes, as always expecting Netlify to do the rest (i.e. building the site and deploying it). Only later that day I realized the builds were throwing an error: <code>window is not defined</code> 😧</p>
<h2 id="the-problem">The problem</h2>
<p>Leaflet uses the <code>window</code> object internally. When trying to do SSR (server side rendering) as Gatsby does, there is no <code>window</code> (because the app is not acually loading in a brower environment). As a newcomer to React and SSR, this was new to me (although it makes sense).</p>
<p>So wherever I need use <code>window.</code> in my code, I have to add checks if <code>window</code> actually exists. This can be done easily in my application code. But, we’re using an external library (Leaflet) here and the last thing I want to do is to mess with the library itself.</p>
<h2 id="the-solution">The solution</h2>
<p>After trying a few different approaches without success, I came across this page in the Gatsby docs: <a href="https://www.gatsbyjs.org/docs/debugging-html-builds/#fixing-third-party-modules">Debugging HTML Builds</a></p>
<p>We can configure webpack to basically ignore certain modules for the build-stage. To do this, add this code to your <code>gatsby-node.js</code> file:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8;overflow-x:auto" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#79B8FF">exports</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">onCreateWebpackConfig</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> ({ </span><span style="color:#FFAB70">stage</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">loaders</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">actions</span><span style="color:#E1E4E8"> }) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (stage </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> "build-html"</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#E1E4E8"> actions.</span><span style="color:#B392F0">setWebpackConfig</span><span style="color:#E1E4E8">({</span></span>
<span class="line"><span style="color:#E1E4E8"> module: {</span></span>
<span class="line"><span style="color:#E1E4E8"> rules: [</span></span>
<span class="line"><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8"> test:</span><span style="color:#9ECBFF"> /</span><span style="color:#DBEDFF">leaflet</span><span style="color:#9ECBFF">/</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8"> use: loaders.</span><span style="color:#B392F0">null</span><span style="color:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#E1E4E8"> ],</span></span>
<span class="line"><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#E1E4E8"> })</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
<p>That’s it. Our Gatsby build works again! 🙃</p>Trigger download of remote images with Next.jshttps://grooovinger.com/notes/trigger-download-of-remote-images-with-nextjs/https://grooovinger.com/notes/trigger-download-of-remote-images-with-nextjs/When the `download` attribute on an HTML anchor is not enough. Also: how to zip on the fly with Next.js API routes.Tue, 13 Sep 2022 15:51:11 GMT<blockquote>
<p>👉 <strong>Heads-up:</strong> This post is probably outdated! Proceed with care.</p>
</blockquote>
<p>Recently I needed to create a component in a Next.js app that triggers a fiel download prompt. Typically, using the <code>download</code> attribute on the link to the file would be enough to trigger a download instead of opening the file directly in the browser.</p>
<p>This would be an example to ask the browser to open the download dialog for the file (instead of opening it):</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#E1E4E8"><</span><span style="color:#85E89D">a</span><span style="color:#B392F0"> href</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"/path/to/my/file.jpg"</span><span style="color:#B392F0"> download</span><span style="color:#E1E4E8">>Download file.jpg</</span><span style="color:#85E89D">a</span><span style="color:#E1E4E8">></span></span></code></pre>
<p>(additionally you could provide a custom filename, like this: <code>download="othername.jpg"</code>)</p>
<h2 id="download-attribute-does-not-work-for-images-on-a-different-origin">“download” attribute does not work for images on a different origin</h2>
<p>As it turns out, the <code>download</code> attribute only works for same-origin resources. In my application the images are living on a different server, therefore this approach fails (i.e. the browser ignores <code>download</code> attribute and simply opens the file directly).</p>
<h2 id="the-solution">The solution</h2>
<p>I decided to use a fairly new feature of Next.js: <a href="https://nextjs.org/docs#api-routes">API routes</a></p>
<p>The aim is to create an API endpoint that takes the URL to the file as a parameter and acts as a simple proxy to the target server. Before sending it back to the user the proxy sets the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition">Content-Disposition</a> header, in order to trigger a download.</p>
<p>Here’s what the final function looks like:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> request</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> require</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"request"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">req</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">res</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D"> // path to file</span></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> filePath</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> req.query.filename;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // filename only</span></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> fileName</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> filePath.</span><span style="color:#B392F0">substring</span><span style="color:#E1E4E8">(filePath.</span><span style="color:#B392F0">lastIndexOf</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/"</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">+</span><span style="color:#79B8FF"> 1</span><span style="color:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // set header</span></span>
<span class="line"><span style="color:#E1E4E8"> res.</span><span style="color:#B392F0">setHeader</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"content-disposition"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"attachment; filename="</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> fileName);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // send request to the original file</span></span>
<span class="line"><span style="color:#E1E4E8"> request</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(process.env.</span><span style="color:#79B8FF">REMOTE_URL</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> filePath) </span><span style="color:#6A737D">// download original image</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">on</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"error"</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">err</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#E1E4E8"> res.</span><span style="color:#B392F0">writeHead</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">404</span><span style="color:#E1E4E8">, { </span><span style="color:#9ECBFF">"Content-Type"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"text/html"</span><span style="color:#E1E4E8"> });</span></span>
<span class="line"><span style="color:#E1E4E8"> res.</span><span style="color:#B392F0">write</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"<h1>404 not found</h1>"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8"> res.</span><span style="color:#B392F0">end</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8"> })</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">pipe</span><span style="color:#E1E4E8">(res); </span><span style="color:#6A737D">// pipe converted image to HTTP response</span></span>
<span class="line"><span style="color:#E1E4E8">};</span></span></code></pre>
<p>Now, I can link to <code>/api/proxy?filename=public/mybeautifulpicture.jpg</code> to trigger a download prompt in the browser, even if the file is on a different domain.</p>
<blockquote>
<p>Be aware of potential <a href="https://en.wikipedia.org/wiki/Server-side_request_forgery">Server Side Request Forgery (SSRF)</a> vulnerability when using the url to the file directly. Thanks to <a href="https://twitter.com/_thomaskonrad">Thomas Konrad</a> for pointing this out! 🙏</p>
</blockquote>
<h2 id="bonus-zip-multiple-files-on-the-fly-before-downloading">Bonus: zip multiple files on the fly before downloading</h2>
<p>As an addition to above solution, I implemented a way to request multiple files from the remote server, which are zipped up on the fly before delivered to the user. Using this approach, there’s no need to do a cleanup job to remove generated zips from the server after the user has downloaded them.</p>
<p>Using the following script, I can request a zip of multiple files by sending a POST request e.g. to /api/zip with a body of <code>{files: ["file1.jpg", "file2.jpg"]}</code>.</p>
<blockquote>
<p>This is a proof of concept implementation. You might want to add some checks like an allow-list and limits to prevent potential malicious usage.</p>
</blockquote>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#6A737D">// file: api/zip.js</span></span>
<span class="line"><span style="color:#F97583">var</span><span style="color:#E1E4E8"> async </span><span style="color:#F97583">=</span><span style="color:#B392F0"> require</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"async"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583">var</span><span style="color:#E1E4E8"> request </span><span style="color:#F97583">=</span><span style="color:#B392F0"> require</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"request"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583">var</span><span style="color:#E1E4E8"> archiver </span><span style="color:#F97583">=</span><span style="color:#B392F0"> require</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"archiver"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">req</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">res</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // name of final zip file</span></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> zipFileName</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> "downloads.zip"</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // check for "files" in request body</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (req.body.files </span><span style="color:#F97583">==</span><span style="color:#79B8FF"> undefined</span><span style="color:#F97583"> ||</span><span style="color:#E1E4E8"> req.body.files </span><span style="color:#F97583">==</span><span style="color:#9ECBFF"> ""</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#B392F0"> outputError</span><span style="color:#E1E4E8">(res);</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // split up files</span></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> filesArray </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> req.body.files.</span><span style="color:#B392F0">split</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">","</span><span style="color:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // check if files is an array</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">Array.</span><span style="color:#B392F0">isArray</span><span style="color:#E1E4E8">(filesArray)) {</span></span>
<span class="line"><span style="color:#B392F0"> outputError</span><span style="color:#E1E4E8">(res);</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // prepend every file with the base url of the remote server</span></span>
<span class="line"><span style="color:#6A737D"> // this assumes REMOTE_URL is set as an environment variable</span></span>
<span class="line"><span style="color:#E1E4E8"> filesArray </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> filesArray.</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">f</span><span style="color:#F97583"> =></span><span style="color:#E1E4E8"> process.env.</span><span style="color:#79B8FF">REMOTE_URL</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> f);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // set content-disposition header</span></span>
<span class="line"><span style="color:#E1E4E8"> res.</span><span style="color:#B392F0">setHeader</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"content-disposition"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"attachment; filename="</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> zipFileName);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // zip them files</span></span>
<span class="line"><span style="color:#B392F0"> zipURLs</span><span style="color:#E1E4E8">(filesArray, res);</span></span>
<span class="line"><span style="color:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">/**</span></span>
<span class="line"><span style="color:#6A737D"> * Zip files and send it as response</span></span>
<span class="line"><span style="color:#6A737D"> * </span><span style="color:#F97583">@param</span><span style="color:#E1E4E8"> urls</span><span style="color:#6A737D"> {array} files to zip</span></span>
<span class="line"><span style="color:#6A737D"> * </span><span style="color:#F97583">@param</span><span style="color:#E1E4E8"> outStream</span><span style="color:#6A737D"> the response object</span></span>
<span class="line"><span style="color:#6A737D"> */</span></span>
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> zipURLs</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">urls</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">outStream</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583"> var</span><span style="color:#E1E4E8"> zipArchive </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> archiver.</span><span style="color:#B392F0">create</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"zip"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8"> async.</span><span style="color:#B392F0">eachLimit</span><span style="color:#E1E4E8">(urls, </span><span style="color:#79B8FF">3</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#F97583"> function</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">url</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">done</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583"> try</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583"> var</span><span style="color:#E1E4E8"> stream </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> request.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(url);</span></span>
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="color:#B392F0"> outputError</span><span style="color:#E1E4E8">(outStream);</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8"> stream</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">on</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"error"</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">err</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#B392F0"> done</span><span style="color:#E1E4E8">(err);</span></span>
<span class="line"><span style="color:#E1E4E8"> })</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">on</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"end"</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#B392F0"> done</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#E1E4E8"> });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // Use the last part of the URL as a filename within the ZIP archive.</span></span>
<span class="line"><span style="color:#E1E4E8"> zipArchive.</span><span style="color:#B392F0">append</span><span style="color:#E1E4E8">(stream, { name: url.</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">^</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*</span><span style="color:#85E89D;font-weight:bold">\/</span><span style="color:#9ECBFF">/</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">""</span><span style="color:#E1E4E8">) });</span></span>
<span class="line"><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#F97583"> function</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">err</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (err) </span><span style="color:#F97583">throw</span><span style="color:#E1E4E8"> err;</span></span>
<span class="line"><span style="color:#E1E4E8"> zipArchive.</span><span style="color:#B392F0">pipe</span><span style="color:#E1E4E8">(outStream);</span></span>
<span class="line"><span style="color:#E1E4E8"> zipArchive.</span><span style="color:#B392F0">finalize</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">/**</span></span>
<span class="line"><span style="color:#6A737D"> * Output 404 Error</span></span>
<span class="line"><span style="color:#6A737D"> * </span><span style="color:#F97583">@param</span><span style="color:#E1E4E8"> res</span></span>
<span class="line"><span style="color:#6A737D"> */</span></span>
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> outputError</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">res</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#E1E4E8"> res.</span><span style="color:#B392F0">writeHead</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">404</span><span style="color:#E1E4E8">, { </span><span style="color:#9ECBFF">"Content-Type"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"text/html"</span><span style="color:#E1E4E8"> });</span></span>
<span class="line"><span style="color:#E1E4E8"> res.</span><span style="color:#B392F0">write</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"<h1>Whoops, something went wrong</h1>"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8"> res.</span><span style="color:#B392F0">end</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
<p>Let me know what you think about this approach by leaving a comment below.</p>Cypress Testing Library Custom Error Messagehttps://grooovinger.com/notes/cypress-testing-library-custom-error-message/https://grooovinger.com/notes/cypress-testing-library-custom-error-message/How to reduce logging noise when using Cypress Testing LibraryTue, 13 Sep 2022 15:42:22 GMT<p>Cypress Testing Library outputs a rather verbose log message when it can’t find an element using e.g. <code>findByRole</code>. The intention is to help you, the developer to fix the test and figure out which accessible elements <em>are</em> available.</p>
<p>However, I find this not very useful and too verbose, especially in a CI environment.</p>
<p>Luckily, Cypress Testing Library allows for customization of the message.</p>
<p>I use this configuration to limit the log messag length, while still keeping the relevant information about <em>which</em> element was not found:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D">// file: cypress/support/commands.js</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#9ECBFF"> '@testing-library/cypress/add-commands'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { configure } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@testing-library/cypress'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">configure</span><span style="color:#E1E4E8">({</span></span>
<span class="line"><span style="color:#B392F0"> getElementError</span><span style="color:#E1E4E8">: (</span><span style="color:#FFAB70">message</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">container</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // truncate everything after 'Here are the accessible roles:'</span></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> indexOfRoleListMessage </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> message.</span><span style="color:#B392F0">indexOf</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'Here are the accessible roles:'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> shortMessage </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> message.</span><span style="color:#B392F0">substring</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">, indexOfRoleListMessage);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // return a new error with the shorter message</span></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> error </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> Error</span><span style="color:#E1E4E8">(shortMessage);</span></span>
<span class="line"><span style="color:#E1E4E8"> error.name </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> 'AccessibleElementNotFoundError'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8"> error;</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8">})</span></span></code></pre>