Will Browar https://wbrowar.com/ https://wbrowar.com//theme/logo.png Will Browar https://wbrowar.com/ RSS Feed for all articles on wbrowar.com en-US Fri, 09 Feb 2024 21:27:09 -0500 Fri, 09 Feb 2024 21:27:09 -0500 Wall-Mounted Standby https://wbrowar.com/article/maker/wall-mounted-standby Sun, 28 Jan 2024 09:51:00 -0500 Will https://wbrowar.com/article/maker/wall-mounted-standby In 2023, Apple introduced a feature to their MagSafe-compatible iPhones, called Standby. This feature lets you use the widgets from your apps when your phone is:

  • Magnetically attached to a MagSafe charger or plugged in with a charging cable
  • The phone is positioned upwards and in the horizontal position
  • The device’s Lock Screen is on

I currently have 2 MagSafe stands by TwelveSouth that I use on a nightstand while I sleep and on my desk while I'm working. With these stands in places here are some ways I typically use Standby:

  • On my nightstand I display a digital clock alongside either the weather widget, a charging widget, or my calendar (via Fantastical).
  • While I'm working I typically leave my phone on the World Clock, showing points that represent the timezones of some of the international teammates I work with.
  • In both places, I’ll turn on the Now Playing screen for audio books and podcasts that I listen to (usually air playing to speakers or AirPods Pros).

I really love how simple this feature is to use and now I want to put MagSafe stands all around my house. After upgrading my nightstand MagSafe stand to the HiRise 3 Deluxe, I gave the Forté I was previously using to my wife, which—replacing her MagSafe charger—left us with an extra MagSafe puck to spare.

Finding a Use Case

My first thought was to pick up another Forté and find a place to put it in the kitchen, but then—as part of a customer journey—marketing for a different brand had another idea in mind.

I got a marketing email by a company I bought iPhone stands and AirTag cases from, Elevation Lab, for a new product that lets you pin an AirTag to your kids or a piece of luggage. While I'm not in the market for that, it got me to visit their website and check it out. While I was there, I came across something they call, MagBase.

MagBase is essentially a little silicon holder for a MagSafe puck that is meant to live on your desk or another flat surface. It’s designed so you can pick up your phone by sliding it off the magnet, but just as easily, you can pick up both your phone and your MagSafe puck to do things like check your email or respond to messages.

I like that the MagBase is very minimal, and even though it’s designed to sit on a flat surface it gave me an idea on how I can solve a very minor problem, while also giving me another place to use Standby.

A few years ago I put together this little couch-side shelf, and above that I mounted a shelf for more vertical space. Last year, I unmounted the shelf for a few minutes to add a headphone hook to it. I use this shelf while watching TV or playing video games and I usually place things like my iPad and AirPods Pro case on it.

As far as my phone goes, I usually put that on the arm of the couch because I had no other particular place to put it. This extra MagSafe puck helped me find a new place for it, and it looked cool to boot.

BZ2 6166

The headphone hook sits in the middle of the shelf, so when sitting on the couch there is some room on the wall to the left and to the right of the headphones. The space to the right seemed like a really good place to put a MagSafe puck. Before ordering a MagBase I thought that maybe I would 3D print my own MagSafe mount. After taking a look at some of the 3D printing model sites I found that the idea of mounting a MagSafe puck on the wall wasn't a new idea, and there were several free, really nice models to choose from.

One thing the MagBase had going for it, though, was that it also comes with a USB-C extender. This would be important because the 1-meter distance from the nearest outlet to the place I wanted to mount the MagSafe puck was too short. That got me to go for the MagBase, so I placed an order and it showed up a couple of days later.

I thought for a little bit about how I would run the MagSafe cable around the shelf. I didn't want to drape the cable over the shelf, but if I added a little gap between the shelf and the wall I could run the cable behind the shelf. This wasn’t really my favorite option, so I unmounted the shelf and brought it down to the workshop to see if I could come up with something else.

Channeling a Solution

I took a look at where the shelf mounting hardware fits into the shelf and noticed that the area I routed out for the mounting hardware gave me about 5mm to work with. With the cable plugged into the wall on the left side, I could have ran the cable from the left mount to the right, leaving the cable just under the shelf.

Maybe it was where I was standing, but something else came to mind. Did you know that the width of the kerf on a table saw blade is exactly the same thickness as a MagSafe cable? At least this is true for the blade that came with my DeWalt table saw.

I moved the table saw fence close enough that the blade lined up with the routed area for the mounting hardware. I raised the blade to just about 3-4mm, picked up one side of the shelf, and did a plunge cut from one side of the mounting hardware slot to the other.

BZ2 0265

I did a quick test of the cable to find that it was a perfect fit!

BZ2 0269

I realized I had to create an exit point for the cable on both sides, so I grabbed a chisel and diagonally cut out a notch for the cable on both sides.

BZ2 0277

While doing a dry run with the cable, I also realized that I would need to let some of the cable sit within the routed out area that the bottom of the mounting hardware sits. The problem with this is that a lot of the stability for the shelf is based around clamping down on that area and I didn't want to crush the MagSafe cable to get this to work.

I found a washer big enough to fill up that area and found that if I stack up two washers it was just about as high as the MagSafe cable. I used some hot glue to attach the washers tougher, then onto the shelf. I didn’t really need to do this but this would make it so that when I'm mounting the shelf the washers wouldn’t move around and fall out of place.

BZ2 0281

After another quick test I was all set with routing the cable from one end of the shelf to the other.

BZ2 0283

The final step was to re-mount the shelf and then attach the MagBase to the wall. I took one more pass at running the cable through the channel I just made. I made it as flush as I could and made sure the MagSafe puck was the right distance from the shelf to where I wanted to put it.

BZ2 0286

Fin

This is a DIY situation I really like being in. This is the 4th time I’ve iterated on this shelf area and each time it becomes more useful for my needs. I also like that everything in this project is reversible. As with all technology, a new things comes along every few years and sometimes things just break. If I decide I no longer want the MagSafe puck in this spot, I can unmount it and remove it from the shelf, leaving only the channel and the notch cut out, but hidden in the back of the shelf. Removing the washers should be pretty easy, too.

BZ2 0300
BZ2 0301

As I was going through these pictures on my laptop, I placed my phone up on the wall, put on a podcast, and got to enjoy this new setup right away.

]]>
CSS for Me, in 2023 https://wbrowar.com/article/code/css-for-me-in-2023 Thu, 04 Jan 2024 18:00:00 -0500 Will https://wbrowar.com/article/code/css-for-me-in-2023 Jamstacked

In late 2019 the agency I worked for went all in on headless and Jamstack sites. At that time we explored ways to pair Craft CMS with front-end frameworks, like Gridsome, Gatsby, and Nuxt. I used my personal website, wbrowar.com, as my guinea pig, giving me a playground to connect all the puzzle pieces involved in creating a headless and static Jamstack website, such as:

  • Organizing the repos and CI/CD for the back-end and front-end source code.
  • Rendering a static front-end that can dynamically pull from the GraphQL API when using Craft’s Live Preview.
  • Working out incremental builds to reduce time taken during static generation.

I also used my site to explore using Tailwind CSS as the primary method for working with CSS. This worked great when paired with the Vue-based components in the Nuxt-based front-end I decided to go with.

2023 CSS Challenge

2023 has been an epic year for CSS features and web components seem to be all over the front-end zeitgeist right now. I keep reading about all of the new tools us front-end folks have at our disposal and I decided to refactor my personal site to give me a place to try them out. This got me thinking about how I'll do this with the Tailwind-based setup.

First off, Tailwind is not an either use it or don’t use it technology. What I like about Tailwind is that you could decide to style every part of a fully custom site with it, or simply use Tailwind for a few utility classes here and there—mixed in with the rest of your CSS. Yes, there is the part that includes the build step, but with my experience—and with build tools like Vite around—I'm not adverse to post processing my CSS.

But with this refactor, I decided that I wanted to go back to the days before I started using Tailwind, and even earlier in the days before I used SCSS for most of my projects. I wanted to see what I could come up with, architecting a front-end using modern CSS practices. While I'm not shooting for complete Vanilla CSS, I wanted to get back to thinking through a system instead of working within a framework.

Markup Templating

In my day job I use TypeScript, Vue.js, and REST/GraphQL APIs all day, so while that makes maintaining a Jamstack site within my wheelhouse, I no longer felt like I was getting the same level of new learning out of my Jamstack setup as I did back in 2020.

The back end of the site is built in Craft CMS and one of the things I like about Craft is that it’s focused on content management and it doesn't care about what you do on the front end. You can use Craft’s built-in routing and Twig templating setup, you can use the built-in GraphQL setup to feed a headless front end, or you can use something like the Element API plugin to expose your content via JSON API to a headless front end. As the developer, it’s your choice on whether or not you use a front-end framework, and if so, which framework you prefer (with a few caveats around things like working with Live Preview).

Nuxt is really great for static sites, and for a time I also looked into using Astro, but in this new refactor I wanted to get back to defining the markup I need, instead of working within a component-based framework. Part of this goes back to wanting to write CSS for my content and another side of it came from looking at what I need my site to do and what kind of content I was creating. I decided to go back to a non-headless site, built in Twig, with the use of the Craft plugin, Blitz, to handle static caching and cache invalidation.

JavaScript Framework is _undefined_

With my HTML markup approach decided, next up I wanted to think about how I’ll work with JavaScript for the places where it’s needed. Looking at my current needs, I really only used JavaScript for highlighting code blocks and some minor functionality around image carousels (which is an enhancement on a CSS scroll-snap-based carousel). Programming these couple of bits in vanilla JS wouldn’t be too much work, but rendering the whole page in something like Vue.js is probably overkill.

I like the general setup of Vue components, and in my recent Craft CMS plugin work I got some experience working in Lit to build out web components. So I decided to use web components where it made sense, then go to vanilla JS for more global interactions. SPOILER: So far I haven’t had the need for any global JavaScript and I wound up using just 4 web components for all of the JS on this site:

  • bokeh-box: a component used to generate bokeh-style bubbles for the background of the site.
  • bokeh-controls: a small set of buttons used to define the color scheme of the site and the color of the bokeh bubbles in the bokeh-box component.
  • code-highlight: a wrapper around a <pre><code> block that enables highlighting for a specific language, using highlight.js.
  • element-carousel: takes all of the child elements within the component and turns them into a CSS flex and scroll-snap carousel. The main JavaScript portion is in observing the element currently scrolled in the middle of the page, then indicating that with some dots at the bottom.

My 2023 CSS Solution

I took some time to think about how I’d structure my CSS. This post by Dave Rupert was helpful in introducing me to other approaches and I wound up with a mixture of CUBE principles, organization like Vue’s Single File Component (SFC), and some inspiration from Tailwind.

The sections below describe the problems and areas of my current CSS solution.

Keeping the CSS With the Twig Pieces

One of the features of SCSS that I’ve utilized a lot in the past was that you could break out CSS into a file structure that mirrored the structure of your templates, instead of creating one long CSS file, broken up with comments and other wayfinding tricks. Vue’s SFCs took this even further in that you could write the CSS relevant for a component directly within the component itself.

The value in either approach is in the time saving of being able to open a template file and then work right within its corresponding CSS that is limited to just the scope that the template file is responsible for.

Since I'm working in Twig files, I don't have an easy way to store my CSS in the Twig template files then extract them into a cacheable .css files that can be served on my front-end. I know there is probably a solution here around using PHP or using something like Node to extract the CSS from the files in the filesystem, but the benefit of writing CSS into a Twig file isn’t worth adding a complicated setup into the mix.

I looked for ways to organize CSS natively, but the closest I could get was to use CSS @import, however, the action of importing happens in the browser, so that would A. lead to some performance woes, and B. wouldn’t work because my Twig templates are not stored in a directory that is exposed to my server’s web root.

There are lots of ways to concatenate and minify CSS with build tools, so at this point I decided that I'd use Vite to handle those tasks.

Vite comes with PostCSS, along with the postcss-import package, that lets you inline files imported via CSS @import. Vite also handles minification during the build step, so between the stock setup I would be all set. The only other package I might consider adding is Autoprefixer, but before doing that, I found that Vite 5 comes with Lightning CSS (included as an experimental feature).

With a solution for bundling CSS in place, I looked at my Twig setup and came up with a structure for my CSS files. My templates directory looked something like this:

templates
├── components
│   ├── article-body.twig
│   └── article-thumb.twig
├── includes
│   └── admin-bar.twig
├── layouts
│   └── global-layout.twig
├── macros
│   └── icons.twig
└── pages
    ├── about.twig
    ├── article.twig
    └── home.twig

My Twig structure could be defined like this:

  • components: self-contained, reusable files included via the Twig include or embed tags.
  • includes: one-off includes that don’t belong in any other directory.
  • layouts: page layouts that are extended by templates in the pages directory.
  • macros: Twig macros that are imported and used throughout the project.
  • pages: templates for a specific page or entry section.

Each of these files could require their own CSS, so my idea was to add a CSS file alongside the Twig template as needed. Here's what I wound up with in this first pass:

templates
├── components
│   ├── article-body.css
│   ├── article-body.twig
│   ├── article-thumb.css
│   └── article-thumb.twig
├── includes
│   └── admin-bar.twig
├── layouts
│   ├── global-layout.css
│   └── global-layout.twig
├── macros
│   └── icons.twig
└── pages
    ├── about.css
    ├── about.twig
    ├── article.css
    ├── article.twig
    ├── home.css
    └── home.twig

The name of each CSS file matches the name of its corresponding Twig file.

In the root directory where all of my other front-end source files are stored I have a CSS file that imports all of these files so that these styles are bundled alongside all of the other global styles for the site. With the exception of some web component styles, all of my styles for the site live in one of these files:

front-end
└── src
    ├── css
    │   └── reset.css
    └── wbrowar.css
templates
├── components
│   ├── article-body.css
│   ├── article-body.twig
│   ├── article-thumb.css
│   └── article-thumb.twig
├── includes
│   └── admin-bar.twig
├── layouts
│   ├── global-layout.css
│   └── global-layout.twig
├── macros
│   └── icons.twig
└── pages
    ├── about.css
    ├── about.twig
    ├── article.css
    ├── article.twig
    ├── home.css
    └── home.twig

When bundled, all of these files are imported and minimized, then served on the front end as wbrowar.css (along with a unique content hash for cache busting upon changes).

CUBEish CSS, Nesting, and the Freedom of Cascade Layers

Early into my career in writing CSS I had latched on to the pattern of OOCSS. As I started working with teams of developers we gravitated towards BEM and eventually stayed there for a while. Our rule for writing CSS was if you were going to style something you’d use a class, and with BEM you’d be descriptive and you’d name everything in a way that was unique and described within the context the element is used.

So we'd wind up with markup that looked a lot like this:

In this case .main_menu was sort of a class that gave you context for which <nav> element this was (amongst maybe a .footer_menu or a .secondary_menu). Then everything else gets prefixed with that context along with what the element is (in regards to how it will be used or styled), then any modifiers that describe something unique about the element.

In our minds, this was descriptive and it solved problems like clashing styles and being able to use ⌘+F to hunt for this style amongst all of our stylesheets in our _scss directory. Over time this also led to some other issues that made us less efficient:

  • Our BEM setup was based around very unique class names, but it also meant that we either had a lot of duplicate CSS code to cover common patterns in unique contexts, or it meant a rat king of selectors and modifiers everywhere the minute you moved something or tried to re-use CSS.
  • Naming took a while to get right. Some things are easy when there are only one of them on the page, but when you had common things, like headers, you have to do more to define them into sections like, .content_header, .sidebar_header, .news_card_header. Sometimes designers would make a decision after seeing something in the browser, so maybe the news cards got moved into the sidebar... Now you are either renaming a bunch of classes, or you have some no-longer-obvious element names.
  • As much as some folks may find a bunch of Tailwind classes hard to skim and quickly parse, reading BEM code—sometimes compiled via includes and partials—can also look like a wall of underscores and dashes amongst a lot of repetitive names. Indenting source code helps, but some extra mental processing time is required.
  • Gzipping CSS files is great when working with BEM, but when you don't have the ability to set that up where your static assets are served, you wind up with extra file bloat in both your CSS and in your template files.

Instead of following my old BEM rules, I’ve combined Cascade Layers, nesting, child selectors, and parts of CUBE CSS to come up with my new CSS setup.

To start, I’ve created a set of layers and I've imported all of my CSS files into their designated layers:

@layer reset, admin-bar, globals, components, theme, layout, page;

/*
 * reset: normalize browser defaults, remove margins, set box-sizing: border-box on everything 
 * admin-bar: layers the styles that come with Admin Bar here so they can be overwritten later
 * globals: set project-specific HTML defaults and global classes
 * components: include styles specific to re-usable Twig components
 * theme: define custom properties for colors, fonts, animation, and spacing
 * layout: styles specific to the global page layout
 * page: page-specific styles
 */
 
@import './css/reset.css' layer(reset);

@import '../../templates/components/article-body.css' layer(components);
@import '../../templates/components/article-thumb.css' layer(components);

@import '../../templates/layouts/global-layout.css' layer(layout);

@import '../../templates/pages/about.css' layer(page);
@import '../../templates/pages/article.css' layer(page);
@import '../../templates/pages/home.css' layer(page);

Adding these layers make it so I don’t need to worry as much about clashing styles or using hyper-specific class naming. This lets me go back to using more global classes, like a .button class, knowing that I can overwrite it on the components or page layers later.

There is still the problem of selecting elements within the same layer, and that's where CUBE and child selectors come in. The things I took most from the CUBE were the composition (C) and the block (B) principles, along with the visual benefit of grouping with brackets (as seen in the CUBE documentation).

Take a look at the markup for my About page for example:

{# Intro text ... #}
{# Links to external URLs ... #}

The first thing you might notice are the bracket groupings here. When you look at the source code it makes it really easy to see where the different sections of code are. I set up sort of a framework for thinking about how these blocks work together with the elements on the page. Here’s a general outline for how I’ve organized this:

  • page is a root level starting point and it is the singular name of the folder that this Twig file lives in.
  • Where page describes the directory this file is in, about describes the file name. All of the styles in the about.css file are nested within .page.about.
  • Any child bracket, like [ content ], gets selected using a child selector, .page.about > .content. Then later, .page.about > .content > .grid. This is done via native CSS nesting.
  • .content-container and .prose are global classes that are defined on the globals cascade layer. Because the CSS in about.css is on the page layer, it’s a lot easier to overwrite properties defined on the globals layer.
  • The grid element contains a few <a> elements, so instead of naming those elements with a class like we would have in BEM, this setup makes me confident that I can set properties using this selector, .page.about > .content > .grid > a.
  • I haven’t done anything too complicated yet, but my goal is to limit the amount of child brackets to avoid nesting too much. If needed, I could break things out in to components, but so far that hasn’t been necessary.

Here’s what the full CSS for the About page looks like:

.page.about {
  container-name: page-article;
  container-type: inline-size;

  & > .content {
    margin: 0 auto;
    padding-top: 100px;
    padding-bottom: 100px;
    max-width: 1024px;

    & h1 {
      font-family: var(--font-futura);
      font-size: 2rem;
      font-weight: var(--font-normal);
      color: var(--color-orange);
    }

    & > .grid {
      --svg-width: 1.5em;
      display: grid;
      grid-template-columns: var(--grid-template-columns, 1fr);
      align-items: center;
      gap: var(--xl);
      margin-top: 50px;
      font-size: 2em;

      & a {
        display: grid;
        grid-template-columns: max-content 1fr;
        align-items: center;
        gap: var(--md);
        text-decoration: none;
        color: var(--color-blue);
        transition: color var(--duration) ease-out;

        &:hover {
          color: var(--color-orange);
        }
      }

      @container page-article (width > 600px) {
        --grid-template-columns: 1fr 1fr;
      }
      @container page-article (width > 900px) {
        --grid-template-columns: 1fr 1fr 1fr 1fr;
      }
    }
  }
}

Global Styles and Custom Properties

My main CSS file, wbrowar.css, imports all of the CSS from my templates directories, and it also includes the CSS for the globals and themes layers. My globals layer contains commonly used classes, like an .article-grid class that is used to display article thumbnails across several pages. It also defines basic element styles, like the styles for <hr> elements.

Because I use Markdown for a lot of the content in my site, the .prose class handles styling some commonly used HTML elements rendered by Markdown:

.prose {
  & > * + *:not(hr) {
    margin-block-start: var(--md-rem);
  }

  & :is(blockquote, dd, dl, h1, h2, h3, h4, h5, h6, hr, p, li, pre) {
    max-width: var(--prose-max-width, 70ch);
    font-size: var(--prose-font-size, 1.25rem);
    line-height: var(--prose-line-height, 2);
    text-wrap: pretty;
    color: var(--prose-color, var(--color-black));

    & a {
      color: var(--color-orange);

      &:hover {
        color: var(--color-blue);
      }
    }
    & code {
      padding: 0.4em;
      background-color: color-mix(in srgb, var(--color-blue), transparent 50%);
      border-radius: 0.3em;
      font-family: monospace;
      font-size: 0.9em;
      user-select: all;
      color: var(--prose-code-color, var(--color-blue-800));

      @media (prefers-color-scheme: dark) {
        & {
          color: var(--prose-code-color, var(--color-blue-300));
        }
      }
    }
  }
  & :is(ol, ul) {
    & li {
      & + li {
      	margin-block-start: var(--sm-rem);
      }
      
      &::marker {
        color: var(--color-red);
      }
    }
  }

  @media (prefers-color-scheme: dark) {
    & {
      --prose-color: var(--color-gray-200);
    }
  }
}

The great things about the layer setup is that if I wanted to overwrite a <ul> on a specific page, I won’t need to specify .prose in the selector, but instead I would follow the rules for the file I’m working in. I did include a few CSS Custom Properties here just in case I want to overwrite something and then use that variable to compute something else in all .prose elements.

The name "prose" might seem familiar to someone who uses Tailwind CSS, and specifically the Tailwind Typography plugin. I’m sure other folks have used the term "prose" before Tailwind came along, but that’s where I first heard of the concept and the name for it.

I got a lot of inspiration for my theme layer from Tailwind CSS. Although I wound up not adding any utility classes (yet), I took a lot of naming from Tailwind.

@layer theme {
  :root {
    /* Colors */
    --color-black: oklch(27.68% 0 0);

    --color-gray: oklch(57.51% 0 0);
    --color-gray-100: oklch(91.58% 0 0);
    --color-gray-200: oklch(82.3% 0 0);
    --color-gray-300: oklch(73.49% 0 0);
    --color-gray-400: oklch(66.06% 0 0);
    --color-gray-500: oklch(57.51% 0 0);
    --color-gray-600: oklch(48.37% 0 0);
    --color-gray-700: oklch(30.9% 0 0);
    --color-gray-800: oklch(27.68% 0 0);
    --color-gray-900: oklch(16% 0 0);

    /* OTHER COLORS ... */
  }
}

For starters I kept the concept of color shades. Part of this was a carry-over from the old Tailwind setup, but I also really appreciate the idea of limiting your color palette to a fixed number of shades. While I understand we could use color-mix() to automate shades now, I specifically like going through and defining each shade for myself (although automating it might be a good place to start).

@layer theme {
  :root {
    /* Font families */
    --font-apple: system-ui, sans-serif;
    --font-avinir: 'Avinir Next', 'Avinir', Helvetica, Arial, sans-serif;
    --font-futura: Futura, 'Trebuchet MS', Arial, sans-serif;

    /* Font weight */
    --font-thin: 100;
    --font-extralight: 200;
    --font-light: 300;
    --font-normal: 400;
    --font-medium: 500;
    --font-semibold: 600;
    --font-bold: 700;
    --font-extrabold: 800;
    --font-black: 900;
  }
}

The fonts setup also comes from Tailwind, at least in the weights naming. I'm not opposed to using the numbers directly in my CSS properties, but the naming is just a tad easier for me to look at and immediately put an image in my head as to what name equates to which weight.

@layer theme {
  :root {
    /* Border radius */
    --rounded-sm: .25rem;
  }
}

As time goes on I’ll add more things, like --rounded-sm, to define specific sizes of things I want to use throughout multiple contexts. Using -sm here is a bit of a risk in that it’s possible I won’t want to be limited to sm, md, and lg naming. When that happens I’ll judiciously employ find and replace where I have to 🤓

@layer theme {
  :root {
    /* Animations */
    --duration: 0.3s;
  }

  @media (prefers-reduced-motion) {
    :root {
      --duration: 0.01ms;
    }
  }
}

The --duration property is a little different in that I'm using that as a base for animations everywhere. I shouldn't ever set a new value for it, but instead I should use something like transition-duration: calc(var(--duration) * 1.5). This way animations can be cut down to almost 0 when prefers-reduced-motion is turned on. I thought about doing something like this for opacity and prefers-reduced-transparency, but opacity isn't always set the same way and it changes more often that I handle prefers-reduced-transparency in the situation where it needs overriding.

@layer theme {
  :root {
    /* Sizes for layout spacing */
    --xl: 32px;
    --lg: 24px;
    --md: 16px;
    --sm: 8px;
    --xs: 4px;

    --xl-em: 2em;
    --lg-em: 1.5em;
    --md-em: 1em;
    --sm-em: 025em;
    --xs-em: 0.25em;

    --xl-rem: 2rem;
    --lg-rem: 1.5rem;
    --md-rem: 1rem;
    --sm-rem: 0.5rem;
    --xs-rem: 0.25rem;
  }
}

In my opinion, one of the best uses of utility classes is to work with the 4px-based spacing system that Tailwind and other utility design systems tend to use. I didn’t bring over utility classes because so far I haven’t needed to, but I wanted to maintain some consistency in spacing for things like container padding and grid/flex gaps.

I picked up the idea of using really simple size naming from looking at the CSS found in the Craft CMS Control Panel (CP). While there are places where I use one-off values, these sizes are used for most of the padding and spacing across the site.

I added -em and -rem variants to each size so that I could get the same general spacing size at 100%, but to let those spaces change as the base font size changes.

Fin

I’m still living with this new CSS setup, and I’m not sure this is scalable or the right approach for other types of projects, but so far I’ve achieved my goals of being able to write less CSS, to find CSS quickly when making changes, and to write CSS for the needs of my content.

Along the way I’ve made notes about other side effects that I feel like have occurred for me since making this transition:

  • This is the first time in maybe a decade or so where I’ve used things other than classes for CSS selectors (when I am creating the markup). I really like not naming everything, but I also wonder how much more work this will introduce during times where I move things around.
  • My biggest concerns are updates and maintainability—two of the main reasons why I started using Tailwind in the past. We'll see how nesting and using cascade layers works out when it comes time to update or remove sections. Because I am the only developer for my site, I’m not too worried, but I wonder how this sort of system holds up in a team setting.
  • Nesting with BEM led to a lot of extra CSS, but native CSS nesting (even when using & selectors) doesn’t add nearly as much extra file size, and a lot of the same benefits that I like are there.
  • I went all in on size container queries and it has been great. I’m not opposed to using viewport media queries, but I just didn’t wind up needing any. Things like clamp() and container queries made this possible.
  • I have a dark mode theme, but instead of defaulting to inverting all of the color shades, I went through and manually added prefers-color-scheme: dark queries as needed.
  • color-mix() is a CSS dream. It works so well and I think it will help in so many situations where you don’t have full control over the colors you’re working with (such as colors brought in from third-party libraries).
  • I wrapped all of the styles for Admin Bar in a layer so it’s easier to work with a custom cascade layer setup. I really hope UI libraries and other third-party packages start to do this. Imagine a company that provides a third-party form wrapping all of their CSS in a layer instead of using !important everywhere. In fact, @scope, cascade layers, and container queries makes me glad for all of the developers for marketing sites who currently cringe when getting a message saying "just drop this JavaScript tag onto the page".
]]>
Unboxing the Birdfy Nest https://wbrowar.com/article/ee/testing-the-birdfy-nest Wed, 06 Dec 2023 12:30:00 -0500 Will https://wbrowar.com/article/ee/testing-the-birdfy-nest Intro

Earlier this year I built two bird houses and put cameras in them as gifts for family members. At the end of that post I mentioned that the company I bough the cameras from, Netvue, invited me into their testing program to give feedback on a product, called Birdfy Nest. I received my Nest a couple of weeks ago and I set it up in my back yard. I wanted to write this post as sort of an unboxing and to show some detailed photos of what to expect from the Birdfy Nest.

Unboxing

BZ2 9258

On first impression, the box the Nest ships in was a little heavier than I expected, but it turns out there's a lot in the box. The retail box for the nest fits nicely into its shipping box, then inside the retail box there is the Nest birdhouse and a series of little boxes for the solar panel and the mounting accessories. There were also a couple of other cardboard boxes used to protect the gap between the top of the birdhouse and the vented roof, as well as the outer camera.

BZ2 9259
BZ2 9260

One thing I really appreciated was that they included a ribbon as sort of a temporary handle to help you pull all of this out of the box, instead of having to turn the box upside down and hoping things don't go all over the place.

The box also comes with two instruction booklets. One is an installation guide that walks you through the installation options and how to use the gear that comes with the Nest. The other booklet is the user manual and it includes things like the parts list and some other standard user manual info. One of the things I really liked is that the manuals included information around how to change the predator guard (the doorway with the hole that the bird enters the box) and they give some suggestions on good places to mount the Nest.

Setup

The first thing the instructions tell you to do is to charge the camera unit up. The Nest comes with a USB-A-to-USB-C cable so as long as you have a cell phone charging brick around you’re all set (no need to pull the solar panel out just yet). To access the camera unit you can pull down the main door from the side of the Nest.

BZ2 9274
BZ2 9263

If you want, you can plug the charging cable right into the camera unit, or you can unplug the cables going into it and slide the whole camera unit out of the Nest.

BZ2 9279
BZ2 9281
The camera is split into three parts: the inside camera (shown here), an outside camera, and the box that stores the camera battery and the WiFi transmitter.

With the camera charging, the next step is to set up the outer camera. In order to do this there's a piece of tape you need to remove, then there's a cover that you need to slide off. The instructions make this clear with some really helpful illustrations.

BZ2 9287

For someone with big fingers, installing the outer camera was a tiny bit tricky. The first step is to turn the camera 180 degrees so the lens is facing the front entrance of the birdhouse. There are guide hole that make this easy to do, but the tricky part is that there are four screws that need to be lined up and screwed into the wooden side. There are pre-drilled holes there already but I found my self having a hard time keeping the screws from falling into the hole used for the camera wire. The screws rolled right out, but it took me a few extra attempts to get it right.

Once the outer camera is fastened to the side you can slip the cover back on. Another neat trick is that you can tilt the camera down. To do this you can pull the camera forward a bit, rotate it, then let it snap into place. I tilted it down a notch to point the lens right at the entrance hole.

BZ2 9290

Mounting Accessories

The three accessory boxes were split up like this:

Wall and pole mounting hardware and extra predator guards

BZ2 9265

This box included a mixed bag of an accessories. In this box you’ll find the a charging cable you can use the first time you set up the camera. There's also a screwdriver you can use to mount the Nest and install the outer camera.

The Nest also comes with two alternate predator guards. One has a hole that is larger than the standard one and the other has a hole that is smaller. The smaller hole provides more protection from predators, but it means only smaller birds can fit comfortably through the hole and reside in your Nest. I chose to keep the standard guard on, but I may consider moving to the larger hole to attract larger birds in the future. The user manual shows you how you can access and then replace the guard.

One of the most important pieces in this box is a mounting plate. This plate attaches to another mounting piece on the back of the Nest. You can choose to screw the mounting plate directly onto a flat surface, using the screws included in this box. There is a paper template that you can use to help mark where the screw holes will go.

If you want to mount your Nest onto a pole there are two smaller hose clamps and two that are slightly larger. You can unscrew the clamp all the way and feed it into the mounting plate, then tighten it around the pole you are mounting it to.

Finally, there is a third options, using the tree-mounting strap found in another box.

Tree-mounting strap

BZ2 9267

You can feed the tree-mounting strap through the mounting plate, wrap it around a tree or a larger post, then tighten it up and lock it into place.

I really like all of the mounting options. I planned on mounting my Nest to a pole, but I’m going to keep the other accessories around so in case I need to move the Nest someday I’ll have the flexibility to try one of the other mounting options.

Solar panel

BZ2 9268

The solar panel is an awesome accessory. It saves you from having to remove the camera to charge it up—disturbing the birds residing in your birdhouse. It comes with two mounting options. There’s a screw-on wall mount that can be rotated to face the sun. The solar panel has a really long USB-C cable that means you can mount the birdhouse where you'd like and then mount the solar panel in an area where you'll get optimal sunlight.

The other option is a posable mount that screws into the ¼ inch standard mount on the back of the solar panel. This mount can be bent and wrapped around a pole or a fencepost. Just note that the posable mount can be found in a different box than the solar panel.

Installation

To mount my Nest onto a pole I fed two of the smaller hose clamps into the mounting plate. I also used the posable mount and attached that to the solar panel so I can mount that onto the same pole.

BZ2 9293

Before putting the mount on the pole I checked the orientation with the mount on the back of the Nest.

BZ2 9282

I see that Netvue just announced a poll that would work in this situation, but I had picked this pole up at a local garden center. I also picked up a squirrel baffle, just like I did for my bird feeder. Finally, I 3D-printed the topper to keep rain out of the top of the pole.

BZ2 9296
BZ2 9294

I added the solar panel in a way where I could hide it when you look straight on, but I might move this again after seeing how much sun we get over the next couple of weeks.

BZ2 9297
BZ2 9299

Software

I had already gotten familiar with the Netvue app from my previous experience with the Birdfy Cameras I purchased. Recently Netvue came out with a new app that’s dedicated to their Birdfy line of cameras. It seems more focused on sharing the story of the birds that reside in your Nest, as well as using AI features to help you identify the birds that visit your Nest.

The setup process is quite painless and I had the app connected to my camera in just a couple of minutes. The only thing I haven’t tried yet is the bird recognition AI feature, but I’ll likely turn that on in the spring, when we have some more traffic around the Nest. Within my first week of setting up the camera I had already had a visitor (my guess is it was maybe a House Sparrow).

IMG 2864

One of the features that stood out to me is that when your camera detects motion and captures a clip, the app gives you the option to "Collect" the bird in the clip. When you hit the "Collect" button, it saves the clip and tags it with the species of the bird. My kids—who are huge fans of Pokémon—really like this feature and the idea of building out a collection of the birds around our house.

Fin

The folks behind the Birdfy Nest really went all out to design this birdhouse. When I was doing research for my homemade birdhouses a lot of features that were suggested by other makers included things like adding a screen at the bottom, adding ventilation, adding notches below the entrance hole to help birds climb out, using a specific diameter for the predator guard and entrance hole, having a door that opens up so you can clean it every year, etc...

When I was building the birdhouses I did some of these things, but I didn't get around to adding all of these features (specifically the mesh screen). This birdhouse just comes with all of those things built into it. I can’t think of anything I would have done differently, feature wise.

There is something to be said for learning how, and then building your own birdhouse, but for most folks I think this is the way to go.

BZ2 9303
BZ2 9305

The Birdfy Nest is currently on pre-order at the Netvue Birdfy website and—as of this writing—there’s even a special Christmas edition that’s on sale.

]]>
Admin Bar for Everyone https://wbrowar.com/article/code/admin-bar-for-everyone Sat, 25 Nov 2023 00:30:00 -0500 Will https://wbrowar.com/article/code/admin-bar-for-everyone Screenshot

Admin Bar was my first Craft CMS plugin—dating all the way back to 2015! I used Admin Bar to learn my way through plugin development on Craft 2. This was before there was a Plugin Store and before Composer and Packagist came into the mix.

The reason why I created Admin Bar was because the agency I worked for had just transitioned off from making all of our projects with Drupal 7 and we had started using Craft CMS 2 for our default CMS of choice. Craft provided a much better authoring experience on the CP side of things (for the way we approached websites), but it was missing the toolbar that we were used to in Drupal and on Wordpress sites. As a developer who also made content updates, those toolbars were super helpful, and it was always fun to show a client that you can find a page on your site that needs updating and then use the Edit link in the toolbar to jump right to the edit page.

So on my free time I put together Admin Bar and "released" it via updates to its GitHub repo.

Screenshot edit
Screenshot edit note
Screenshot edit outline

I wound up looking for more uses to expand Admin Bar so I added the Edit Link feature. This was more like an edit button for those thumbnails and summaries of news stories you might put on a news homepage.

Implementing Edit Links was trickier than the regular Admin Bar. For one, there would be multiple links on the page, which meant instead of loading a bunch of CSS and JavaScript multiple times, I had to find a way to load them once, find all of the edit links on the page, then get them to render. I eventually got this all working, but sometimes there were issues.

Around this time I had started using Vue.js on a lot of my projects. There seemed to be a wave of folks in the Craft world using Vue or similar frameworks to add interactivity to their pages, on top of their Twig-based sites. For the most part things still worked, but sometimes race conditions between my Edit Links code and Vue’s rendering would cause some issues.

Screenshot guide entry
Screenshot widget edit links

Along with Edit Links I also experimented with a concept, called Admin Bar Widgets. The idea here was that a plugin, like Guide, could inject some HTML into Admin Bar using a custom event on the PHP side of things. The HTML here could give you some info based on the current page you’re on.

I also tried setting up a widget that would find all of your Edit Links on the page and made it so you could click on the link to jump down to that section on the page.

I hadn’t heard of anybody really using the Admin Bar Widgets feature, and even in my own use I found that I’d run into some CSS issues due to the stylesheet on the page clashing with my widget CSS.

THE Problem

So some of the features in Admin Bar had some issues, but they were all things that could be addressed with some CSS or a little extra JS work. The biggest issue that Admin Bar ran into was more about the shift into Jamstack, Headless, and static-cached sites.

On a Twig-based project, Admin Bar would use the session information in your browser to figure out if you are logged in and what permissions your account had. It would also rely on you passing in an entry argument, or it would try to use Craft’s built-in route matching to figure out what page you were on. Then it used all of your config options and plugin settings to customize the look and the links on the Admin Bar on the page.

At some point Admin Bar needs to get information from your CMS database and PHP files to render itself onto the page. In the world of headless sites, where your front-end may even be hosted on a completely different domain, this surfaced a whole bunch of problems.

I spent a lot of time thinking about this. Could you use GraphQL to render out the Admin Bar for the page, then use some JavaScript to pull that in and display it on the page? Could you hit a plugin controller that returns a blob of HTML, or at least the info you need to pass into Admin Bar on the front-end? How do I create something in the Admin Bar plugin that can do all of this securely? Maybe I could put together some lambda functions and service workers to help with this stuff?

Spoiler for the rest of this article: I don’t have answers to these questions, but I have come up with a way to get Admin Bar onto more projects.

Admin Bar 4.0

My conclusion came down to deciding where the line would be for Admin Bar going forward. It’s not feasible for me to try to solve auth and rendering on static sites across all of the different server setups and front-end frameworks out there, but I could provide a way where if you can get through those barriers in your specific site setup, I can provide the front-end component that is a consistent experience across projects.

After spending some time learning some new front-end tricks I converted Admin Bar’s front-end to a standalone NPM package that can be used on Craft and non-Craft websites. I set up Admin Bar Component as a web component, built on Lit.

Screenshot bar

The web component can be installed, using npm install admin-bar-component --save, and you can bundle it in with your existing JavaScript, or you can use a <script type="module"> tag to add it to the page.

The way web components work is that there's the JavaScript that registers the component and you need this loaded onto the page once. Then you can add an <admin-bar> element and the JavaScript will kick in and render the Admin Bar Component based on the attributes and slot content you pass into it.

So in a headless or static site, you could have a script do a fetch to your Craft CMS back-end and when that returns you can use logic on your front end to add the <admin-bar> element.

Props and Slots

The README file in the repo goes over the options you can pass into an Admin Bar Component to add things like a logout button, or your links to various parts of your Craft CMS instance. Everything is opt-in, so you can choose what the greeting looks like or what buttons show up and in what order.

Admin bar example

To render an Admin Bar Component to look like the one in this screenshot, the code looks like this:

Hello, Mate
EditSettingsCraft CMS Docs

Styling an Admin Bar Component

There’s a lot of talk about web components right now and one approach I really like is to take existing HTML and wrap it in a web component to bring interactions to it that you can’t do without some custom scripting or with native HTML elements. The alternate approach is using the Shadow DOM for your component, which gives you the ability to use slots, but it puts a wall between your web component’s HTML and the CSS stylesheet in your project.

There are pros for rendering web components in the "Light DOM", but the Shadow DOM solves a major issue for Admin Bar in that we wouldn’t want to make a change to our website’s CSS only to find that it screwed up the front-end toolbar, requiring us to fix it again.

Admin Bar Component’s look won’t be affected by your stylesheet, but there are plenty of CSS Custom Properties that give you control to adjust things to your liking.

For example, say you’ve added the environment warning to let your content author know they are editing on the QA site and you wanted to make that warning a little more obvious. You could make the height a little taller:

admin-bar {
  --admin-bar-environment-height: 10px;
}
Admin bar environment height

Oh, wait. You’re actually on DEV. So you change the warning color:

admin-bar {
  --admin-bar-environment-height: 10px;
  --admin-bar-environment-bg-color: rgb(255, 50, 24);
}
Admin bar environment color

Okay, so that’s good, but maybe you don’t like the striped look. That’s cool. You can replace the background CSS to a plain, red line:

admin-bar {
  --admin-bar-environment-height: 10px;
  --admin-bar-environment-bg: rgb(255, 50, 24);
}
Admin bar environment color solid

Admin Bar for Craft CMS

The web component’s superpower is that it’s not build just for React, Vue, or any other specific framework. This also means that it can be used for front ends for non-Craft CMS projects. But for the folks who have used Admin Bar in the past, I wanted to keep the same experience (or as close to it as possible).

Admin Bar 4.0 is the same plugin as the 3.x version, with most of the same config options, but the output has been changed to load Admin Bar as the web component, instead of the old Vue component it previously used.

Screenshot bar

Currently the plugin is in beta, but when it is released you’ll be able to upgrade by changing the version in composer, or by installing it from the Craft CMS Plugin store.

There are a couple of new config settings and a few that have been removed, but for the most part things should work as they currently do. Check out the plugin’s README for all of the updated settings.

Admin bar settings

If you had styled the previous version using the Custom CSS setting, check out the new CSS Custom Property defaults. Most of them are similar to the previous version, but there are name changes, a couple of removed variables, and some new additions.

If you'd like to give the new version of Admin Bar a try, you can install it for a Craft 4 site by updating your composer file with the following:

{
  "require": {
    "wbrowar/craft-admin-bar": "^4.0.0"
  },
}

Fin

Admin Bar continues to teach me new things. This time around the Craft CMS portion was more about cleaning things up, where the web component portion gave me a chance to learn a little more about authoring and using web components.

Admin bar all four

I hope the work here will make Admin Bar more useful for more types of projects. If you're one of the folks who’ve been a part of its 60k+ installs over the years, thanks for helping me catch bugs and for helping me to continue to improve it. This project will continue to iterate, and I’m looking forward to seeing what else can be done in this next phase.

]]>
LITtle Layout: Converting a Craft CMS Field from Vue to a Web Component https://wbrowar.com/article/code/little-layout-converting-a-craft-cms-field-from-vue-to-a-web-component Mon, 06 Nov 2023 11:59:00 -0500 Will https://wbrowar.com/article/code/little-layout-converting-a-craft-cms-field-from-vue-to-a-web-component Little Layout is a plugin for Craft CMS that gives content authors the ability to visually adjust layout options for a given page, content section, matrix block, or for whatever else the developers sets it up to do.

Little layout example center column

The field is designed to be simple to use and flexible enough that you could use it for all sorts of things, like text alignment or CSS flex and grid alignment. On this blog—run on Craft CMS—I use it to align text and other content across a six-column CSS Grid layout.

So if you’re looking on a tablet or desktop-sized screen, you might see this text laid out in a two-column layout—with this text off to the right.

The Problem

While dogfooding my own plugin, I've found what I think is a performance bottleneck. I use several Little Layout fields for each matrix block of content, and each article may include dozens of blocks, therefore the amount of Little Layout fields instantiated and active on the page grows pretty quickly. What I've found is that the more matrix blocks I add, the more I start to see a slow down in performance around the rest of the page. Typing in a textarea text box starts to lag and adding images and links to other relational items takes a few seconds.

I’ve used Craft CMS for a while and it tends to be very performant at its core, so based on the few plugins I use on this personal blog, I have to suspect that my Little Layout fields could be the culprit. I started thinking of potential theories for what seemed to be a memory or CPU leak:

  • For one, I used Vue 3 to create Little Layout’s field and settings page. Each field instance calls Vue’s createApp.mount() method to create a new Vue instance for each field. On a page with maybe 10 Little Layout fields you may not notice any issues with this, but as you create a new matrix block a new field instance is added, and that means a new Vue instance is created and running on the page.
  • Another potential problem could have been a ResizeObserver I had on each field, watching to see if the field would need to horizontally scroll. If it did, this put up a message to the author letting them know they could scroll to the right to access the other fields.

Solutions

For the ResizeObserver issue, I was thinking of coming up with a CSS-based solution. I thought maybe the new :has() selector or some of the new scroll-related CSS features would help, but I haven't I haven’t found the solution for this yet. Because the feature was more of a helper, I removed the observer for now. You can still horizontally scroll, but there won’t be a message that pops up.

Regarding the Vue issue, I spent some time thinking through some potential solutions:

  • Instead of creating a new Vue app for each field, I could find a way to create one Vue app and see if I could share the instance via Vue’s Teleport feature. I sill haven't thought through how this could actually work but I have a feeling the performance gain won’t be very big.
  • Another solution could be to try to switch to the Craft CMS internal Vue asset bundle instead of bundling in my own version of Vue in my plugin JS. I don’t think it will help because I think while it might reduce the JS bundle size it won’t reduce the amount of CPU and RAM used for multiple Vue instances.
  • Finally, after learning a little more about web components, I thought maybe this is a good use case for a set of web components to replace my Vue components. The change of framework shouldn’t affect the content author as long as the way the field works and its output remain the same, but if I can get a little closer to the metal with web components I thought I could at least explore this option by refactoring the Control Panel (CP) field first.

I went with the web component approach first, refactoring from a set of Vue single-file components (SFC) to a set of Lit-based web components. I've pushed this up to Packagist and as I type this blog post I'm going to see if I notice any improvement over the Vue version of the field.

Why Lit?

The short answer is because Dave told me to use it.. I wanted to start with vanilla web standards first, but I quickly found that things like reactive properties and render loops involve a lot of the work that web component frameworks are great at mitigating. Templating would also make a big difference since rendering this field requires loops and conditionals based on user input.

I think it’s still good to learn these standards, but since this isn't a web component I'm sharing as a library I felt like I had a little more freedom to work with dependencies that provide some extra helpers—as long as they are an improvement over my existing Vue setup.

As someone who’s spent time in Vue more than any other front-end framework (other than maybe jQuery), I am very fond of the things a Vue SFC gets you. I was really happy to find some equivalents in Lit that made porting the components over easier.

Register Once, Use Everywhere

So in the Vue version of the Little Layout field I needed to detect when the field was rendered onto the page (via Twig) and then I needed to call a JavaScript function that kicks off initializing Vue and attaching it to that field’s markup. This had to work both when the page initially loads, and whenever a field is created within something like a Matrix or Super Table field, so it was set up to get the name of the newly created field, match that up to an element on the page, then use a JS function to mount the component via Vue.

Web components are much more elegant here. When the page loads I can load a JavaScript file that registers my web component as a custom element on the page, based on the name of my choosing. So I have a script that defines little-layout-field as a custom element and whenever that element shows up on the page the web component will be used to render it. This means when new matrix block creates a new little-layout-field element I can pass in some props to define the settings for the field and rendering happens automatically.

import { LittleLayoutField } from './components/LittleLayoutField'
import { LittleLayoutFieldControl } from './components/LittleLayoutFieldControl';

// Register light DOM field component that includes input element for the page.
customElements.define('little-layout-field', LittleLayoutField);

// Register shared layout component (including layout boxes).
customElements.define('little-layout-field-control', LittleLayoutFieldControl);

Light vs Shadow

As you can see in the snippet above, I’m actually registering two web components to the page. This follow a pattern from the Vue setup where I have a shared component that displays the layout boxes and handles the majority of the author’s interactions, then I have a wrapper component for the content author’s field and another wrapper that is used in the field settings page.

So far with this refactor I've only ported over the content author field, so refactoring the settings field component is to come—if this experiment winds up being the solution to go with.

This pattern also helps with something I was struggling to figure out around web components. By default, Lit renders your template into the Shadow DOM. This is great for scoping your markup and your styles, but I wanted the input field to be rendered outside of the Shadow DOM so it can be accessible if Craft’s built-in JavaScript needs access to it.

In my LittleLayoutField component, I switched the rendering mode to target light DOM with the createRenderRoot() method:

/**
 * Changes the render mode for this component from shadow DOM to light DOM.
 */
protected createRenderRoot() {
    return this;
}

The resulting markup looks like this:

Little layout rendered markup

You can see here where Shadow Content starts within the little-layout-field-control element, but everything else is rendered on the page as normal elements.

In this setup, the content author interacts with elements within the little-layout-field-control element, the field value is computed there, then it's passed up to the little-layout-field component using a custom event (similar to how you could use emit to pass data up in Vue), then little-layout-field renders the input field.

From the Craft CMS standpoint, this input field is part of the overall edit page form and it gets submitted with the computed value. Then the developer can continue to work with that value as they would like in their GraphQL or Twig template.

Matching the Craft CP UI

One of the other challenges I ran into here is making sure my field continued to feel like it was part of the Craft CP design language. I know the layout boxes are sort of unique, but Craft CP has a defined color palette and things like icons that I wanted to continue to use.

The tricky thing with rendering in the Shadow DOM is that because its CSS is scoped, you can’t simply drop in a class that matches up to an existing style in the global stylesheet. For example, you wouldn't be able to get button styles, like the ones you’d get from the btn class. You also wouldn't inherit things like the text colors or global spacing properties. Imagine that there's a wall around everything in the Shadow DOM and you need to find a way to pass things through, or duplicate them within your component.

Luckily the Craft CP includes a whole lot of CSS Custom Properties. Most of the colors used in the CP are defined on the :root as custom properties, so pulling in colors was no issue at all. I had a few places where I wanted to slightly adjust a color and the new CSS color-mix() method came in handy.

The one thing that I did have an issue with is that in the Little Layout field there is a setting that lets you clear out the selected field data and this uses a button with the classes, delete icon on it. The icon class adds a global icon pulled in from a global stylesheet, and the delete icon selects the correct icon and then does some work to make sure it’s centered and positioned correctly.

Little layout field with clearable
Little layout field with clearable hover

This icon and its interactions wouldn’t be hard to re-create, but because the Craft CP styles could change at any point, I wanted to utilize the original styles from the original stylesheet. The way that Craft’s CP resources are loaded includes a unique hash in the file name, so in order to load the stylesheet, I'd need to pull it in via PHP first and then pass the path to it into the web component. While I think the web component could load that CSS file, it would mean a double download penalty for the user.

Again, Lit has a solution that worked in my light DOM/Shadow DOM setup. Lit uses the web standard for using slots to let you pass elements into web components as child elements.

The button in my little-layout-field-control component is rendered like this:

protected render() {
    return html`
        
${this.clearable && this.editable && this._hasSelected ? html`` : nothing}
`; }

Notice that the button doesn’t include the delete icon classes, because this component wouldn’t be able to use them to render the icon in the Shadow DOM. To fix this, a slot, called clear-icon, has been added. The button can be clicked on to handle clearing out the field data, and the contents of the clear-icon will come in from the parent web component.

The parent looks like this:

protected render() {
    return html`
      
    `;
}

Here we are rendering the little-layout-field-control component (as a child component) and passing a plain old span with the delete icon classes on it as the clear-icon slot. Because this file is rendered in the light DOM we'll get any styles added onto the page, even though it gets nested into the markup of the Shadow DOM.

Speaking of CSS

In the Vue version of Little Layout I had used Tailwind CSS to handle everything CSS. I was using Tailwind with a prefix to help namespace my classes but otherwise I did things like copied color values from the Craft CP color palette into my config (this was before Craft’s CP colors were changed to CSS Custom Properties).

While I am generally fine with using Tailwind where it helps you out, using any framework CSS in Shadow DOM-scoped styles is a pain and it requires compromises and jumping through hoops. This is especially true for Tailwind.

So I bit the bullet and removed Tailwind from the web component, in favor of refactoring my template to use vanilla CSS. I was essentially re-creating my HTML markup anyway, so I used my old design as a guide, but wound up re-writing all of the CSS based on the new markup.

Lit lets you add CSS to the Shadow DOM-rendered elements via a styles property, using a css template function, similar to the html template function you use in the render() method. CSS for the light DOM elements can be styled by the page itself.

static styles = css`
  .container {
    overflow: auto;
    -webkit-overflow-scrolling: touch;
  }
  .field {
    --layout-box-size: 22px;
    --layout-boxes-gap: .25rem;

    display: grid;
    grid-template-columns: min-content minmax(0, 1fr);
    gap: .3rem;
    align-items: center;
  }

  // ... more styles
`;

Fin

So while I'm still testing out this approach and learning my way through the technology behind web components, I’m pretty happy with the results. I’d like to reach out to the Craft CMS community to see if anybody there can help beta test this new version of the field before releasing it as an update. Based on how that goes, I will likely revisit the settings Vue component and then fully remove Vue and Tailwind as dependencies on the front-end of my plugin.

If you have a Craft CMS project and you would like to try this out, you can change the version number of wbrowar/craft-little-layout in your composer.json file to use the version in the current development branch:

{
  "require": {
    "wbrowar/craft-little-layout": "dev-main"
  },
}

If you run into any issues, please go ahead and create a ticket in the Little Layout repo: https://github.com/wbrowar/craft-little-layout/issues

]]>
Chest Organizer https://wbrowar.com/article/maker/chest-organizer Wed, 10 May 2023 12:30:00 -0400 Will https://wbrowar.com/article/maker/chest-organizer We wanted to get a storage chest for our back yard because our kids were spending more time with friends out there and we wanted to make it easier for them to access their toys. Along with the better playing-outside weather, we were doing more grilling and I was looking for a place to store grates, charcoal, and other grill-related tools.

At first I planned on getting a separate storage cabinet for the grilling stuff, but we found a storage chest that was big enough to house everything. The tricky part was keeping everything organized and separating the toys from the fire-related tools in one big, empty box.

This project was a quick one and it helped clean out some of the leftover wood I had laying around. It also let me try out something new on the 3D printer.

Design

The basic idea was to cut the interior of the box in half, based on the size of some of the grilling components. Our grill was a 22-inch kettle grill, so the widest we'd need to go is around 22 inches. The grill we have has a grate with a 12-inch hole that is meant for accessories, like a cast iron plate or wok. It also came with a warming shelf that we used on occasion. We also had some cleaning tongs and matches that needed a place to stay.

On the toy side of things, we have various balls, bats, and catching devices. My kids’ reach is still pretty short, so we wanted to elevate all of the toys so they could easily reach in and grab what they need.

BZ2 7746

I put together a rough plan for everything so I knew what sizes I was working with, but along the way I changed things up based on where we wound up putting the chest.

As far as materials go, I had a bunch of leftover plywood and pine 2x4s, so my plan was to whip them all together using some glue and some brad nails, then use some oil or paint to seal them.

Grilling Dividers

I had plywood ripped at about 2 feet wide, which was perfect for the height of the chest. I left the height as-is and worked out the measurements along the length to cut it down into the lengths I needed it to be for each piece.

BZ2 7752

I used some foam packing pieces as a base and a couple of clamps and a long piece of scrap wood as a guide to cut the pieces with the hand saw.

By the time I was done I had cut all of the walls with the hand saw and used the miter saw to cut the smaller pieces down to the right size.

BZ2 7767

I planned on attaching them all together using glue and nails and I didn't go through the trouble to plan out any fancy joinery as the pieces should be plenty supported for their use. I did cut the smallest divider a little wider and then I use a router to cut a channel into its connecting pieces. While maybe unnecessary, it offers a little more support in the middle.

BZ2 7771

I went ahead and nailed these pieces together.

BZ2 7773

Next up I measured out the length and width of what would be the floor of the raised up portion of the grilling side. I cut that down with the hand saw and miter saw and put it aside.

BZ2 7775

At this point I tested the fit inside the chest and everything was looking good.

BZ2 7780

I wanted to add a little support to the raised floor, so I made a frame with a 2x4 and nailed that into the floor piece.

BZ2 7794

With that all set, I nailed the floor into place. Although not shown in this picture, I also added one small 2x4 piece in the corner for a little extra support.

BZ2 7795

I flipped the piece over and used a rounding bit along the tops of all of the dividing pieces to smooth them down a bit.

Making a Toy Table

Once I had the final size of the grilling side set, I used the remaining room inside the chest to create a platform for the toys.

I cut a leftover sheathing panel down the the right width and length to cover the toy side, then I used some 2x4s to create four legs and then some supports.

BZ2 7796

I used some 1-2-3 blocks and clamps to hold them up while I nailed the supports into place.

From there I thought about how to get the table in and out and went over to the drill press—which still had the 1½ inch hole saw bit in it from my birdhouse project—and cut a whole on one side of the middle of the table.

I used the same rounding bit on the router to smooth out the hole so it no longer had any sharp edges.

BZ2 7800

Quick Finish

Just like everything in this project so far where I was using scraps and leftovers I also chose to finish everything with some leftover spray paint. I had a couple of cans of red and black paint, but the black had primer built in so I decided to go that route.

BZ2 7802
BZ2 7803

A couple of coats later, I let the pieces dry and then I placed them into the storage chest.

BZ2 7816

Functional Labels

In the grill side of the chest I wanted to hang a couple of tools, so I needed some sort of hook in the portion that was the full height of the chest. I decided to design and print the hooks with my 3D printer.

I used Fusion360 to create a model that would wrap around the top of the plywood edges with a hook protruding out one side.

BZ2 7830

The fit came out great and the hook seemed strong enough for the things I would hang off of it.

I also wanted to test out some debossed lettering with my 3D printer, so I made an SVG label, imported it into Fusion360, and cut it into the base of the hook. I figured that if the icon was printed on the 3D printer’s plate it would come out clearer.

BZ2 7820

There was one part that got a little messed up in the print, but I figured that this was good enough to proceed with, so I made labels for some of the different sections and I printed those out using PLA.

BZ2 7824

It took a few tries to get the print right and I wound up settling with a couple of pieces that had some errors on them.

The thing I learned here was not to fill up the printing plate with too many pieces. Also, I will still need some more practice printing out text and next time I might consider printing it on a different side or rotating the print 90˚ so the text isn't printed on the plate.

Fin

BZ2 7825
BZ2 7828

If I were making a storage chest from scratch I might consider taking more time to focus on stability and finish, but I'm happy with the amount of effort I put towards making this work.

Eventually I plan on taking the platform out of the toy section and I left a little extra room on the grilling side for future accessories. For now this simple project will help keep things tidy for both play time and grilling time.

]]>
Solar-Powered Bird House https://wbrowar.com/article/maker/solar-powered-bird-house Tue, 02 May 2023 07:00:00 -0400 Will https://wbrowar.com/article/maker/solar-powered-bird-house A few months ago I created this Mid-Century Bird Feeder and showed the finished photos to some family members. Upon checking it out, my mom made a request: she asked for a birdhouse made specifically for North American bluebirds. My sister was with us and asked for one, too.

I had a bunch of ideas for some cool and colorful bird houses, but after doing some research I landed on a lot of parameters and pointers that make for a successful bluebird house. So I dialed my ideas back a bit and—despite sticking to some standard birdhouse plans—this project led to a new tool and some cool experiences.

Design

In my research I found several great sites that shared the basic plans for bluebird houses. I made note of the optimal dimensions for the inside of the birdhouse and the height of the entrance hole. I also read about a few features you can add to help keep the entrance hole safer from predators, as well as a way to help newborn babies leave the nest for the first time.

One thing that caught my eye was that a lot of makers had added cameras to their birdhouses and bird feeders so they can see their avian guests up close. This was a whole entire rabbit hole that included lots of options, ranging from all-in-one camera kits to some how-to articles for creating your own DIY camera feed. Since this was a gift, I was leaning towards the camera kits that had software that was simple to set up and use.

With that in mind, I left the design of the bird house a little open-ended until I was sure as to what the camera hardware setup would look like.

BZ2 7599

Technology Testing

I did some shopping around when looking for a good camera kit to use. A lot of makers had something similar to a Wireless Bird Box Camera, so I started my search there. These looked very compact and there were lots of options to choose from.

I liked some of the AI bird feeders, like the Bird Buddy, that include things like bird recognition and activity notifications. However, Bird Buddy didn't sell a stand-alone camera kit and being a bird house, the AI recognition feature would go unused.

I eventually stumbled upon Netvue’s Birdfy Cam and it seemed to be exactly what I was looking for. I liked the idea of using a solar panel as a way to keep the camera battery charged up, so I bought the bundle that included both the camera and the panel.

IMG 2082
IMG 2081

The camera used a standard ¼ inch mount, so I used a couple of flexible lighting mounts to clip the camera and the solar panel to my bird feeder. I wanted to test the overall experience of capturing clips, streaming, and getting notifications via the app. Here are my general notes:

  • The software made things really easy to set the camera up and to change options. I really liked that you could flip the camera upside down and set the recording quality.
  • I liked that you could quickly save clips and that on iOS it created an album in Photos for storing the clips you saved.
  • The solar panel did its job just fine. While I don’t live in the sunniest area, we still got plenty enough to charge the camera and to keep it charged for the entire time I tested it.
  • The clips are only 1080p, and I hope one day they offer a camera that lets you record in 4k (even if it only streams in 1080p), however, recording at HD quality was still pretty good and I think most people would prefer better battery life over image quality.

Throughout the day it was fun to see the variety of birds that visited my feeder.

Next up I wanted to make sure the camera would work within the rough dimension of the birdhouse and I wanted to figure out the best viewing distance from the bottom of the bird house to figure out the final measurements.

I used my sketch to cut a cardboard box into a 5-by-5 inch tunnel and placed the camera at the top. With my phone streaming the camera feed, I marked the height that I thought worked the best.

BZ2 6986

I cut a hole in the cardboard and stacked up a couple of 1-2-3 blocks at the bottom of the model to get an understanding of what the height of a nest or some eggs might look like.

IMG 2096

From there I had finished up my plans and started gathering materials to build two birdhouses.

Building the Base

I wanted to use cedar for the birdhouses as it holds up well without any finish. I know the sun will eventually fade the color a bit, but I still picked pieces that had nice grain patterns and as few knots as possible.

BZ2 6993

I picked out lumber that was close to the width in my plans, and I trimmed down the rest of the pieces as needed.

BZ2 6999

I had figured out the height for the sides and the front and the back, so I used the miter saw to cut two piles—one for each birdhouse.

BZ2 6998

There was a slight color difference between each pile of wood so I decided that throughout the whole project I would try to keep the wood from getting mixed between each pile. This way each birdhouse would look more cohesive than if there was one or two pieces that had a very different grain pattern.

BZ2 7001

Once I had the walls done, I set my miter saw to a 30 degree angle and cut the diagonal top of each of the side walls.

BZ2 7004

I set my table saw to a matching angle and cut the tops of the front and the back down to match the side walls.

BZ2 7010

Because I was planning on using wood glue and brad nails to join the walls together, I wanted to give the bird feeder some extra support by cutting some rabbets into the bottom piece. To do this I first cut a deep cut onto the sides of the piece, then flipped the piece onto its side, adjusted the fence, and did a very shallow cut to trim off the hanging slice of wood.

BZ2 7012

I did another cross cut to add a trench into the front and back walls at the height to match the bottom piece.

BZ2 7015

I didn’t get it quite right the first time through, so I wound up trimming things down a little bit to fill in some of the gaps that showed up when I first put it together.

BZ2 7023

Once I got things to fit, I tested out the bottom and things lined up nicely.

BZ2 7022

WiFi-Enabled Roofing

With the walls and floor cut to size, I did another test of the camera position.

IMG 2101

This time I had to factor in where the camera’s antenna would need to go, as well as to make sure I had room to plug in the cable for the solar panel.

BZ2 7024

I grabbed the back wall and used my router to create a channel for the cable to to live in, as the camera plus the cable wound up being about a ¼ inch too tall.

BZ2 7026

At the top of the channel, I used a slightly larger cutting bit to create space for the antenna to stick out the back of the birdhouse.

BZ2 7027

My plan was to mount the camera onto the roof and make it so the roof could be pulled out without disturbing anything going on in the bottom of the birdhouse. I don’t think any bird would tolerate any disturbance to their nest, so this gave better access to the camera in case some technical support was needed.

I still had to figure out exactly how to mount the camera to the roof, but I did know the dimensions I’d be using for the top piece.

BZ2 7029

The lumber I had wasn’t wide enough to allow for an overhang along the sides the birdhouse, but luckily I had a really long and narrow piece of scrap cedar laying around. I cut the strip down to the same length of the roof piece and then glued two strips onto either side of the main piece.

BZ2 7031

After cleaning up some glue drips I ran the roof pieces through the planer to smooth them down and to get them to the thickness I wanted.

BZ2 7035

Once again I tilted the blade on my table saw and did a cross cut to match up the angle on the top and bottom ends of the roof pieces.

BZ2 7043

Speaking of Access

One of the things I remember from the birdhouse we had when I was growing up is that every year we would clean out the inside of the birdhouse to get it ready for the next bird who would come and make it their home. To do this we had a hinge on the front panel of the birdhouse and we would lift that up for access to the inside.

I wanted to add some hinges to the left side panel so you could open it up like a cabinet door. I wanted to avoid a big latch on the front of the birdhouse, so I decided to try using a magnet to keep the door shut throughout the year.

BZ2 7045
BZ2 7048

At first I thought I had picked out a strong magnet and a metal plate that would do the trick, but after actually getting it screwed into place I realized that the screws made it so the magnet wouldn't get close enough to the metal plate to provide a strong enough connection to hold the door on securely.

I wound up finding better hardware with a bigger magnet, more surface area, and recessed screw holes that make the screws flush with their surface.

BZ2 7050

I used a chisel to widen up the holes used for the original magnet.

BZ2 7052

Door Groove

To make it a little easier to open up the door, I wanted to use my router to create a groove along the bottom-front of the side door. While I didn't have a router table, I did have a guide attachment to my router that got me pretty close to a router table fence.

BZ2 7058

I lined up fence, turned the router on, and then slowly dropped a test piece onto the bit and pulled it across the guide.

BZ2 7060

To my delight, this worked as planned! I grabbed the side door pieces and carefully repeated my movement until I created the groove at a few inches in length.

BZ2 7063

Ventilation

While I was working on the sides, I wanted to add some air holes to the top of each side for some extra ventilation. I wanted to drill holes in the shape of a circle using my drill press. While I know you could use a compass and some lines to map out the coordinate for each hole, I chose to go the visual route using a drawing app on my computer to add points to a circle and I printed that out a few times across a piece of paper.

BZ2 7054

I taped down the paper to the wood and used a center hole punch to mark where each hole would go.

BZ2 7055

I pulled the paper off of the wood and drilled each hole where the marks were made.

BZ2 7056

I cleaned up the holes a little bit, but I knew that I would eventually be sanding these pieces so I could come back and fix them along with that step.

BZ2 7057

Extra Support

At this point, because of the door, the base of the birdhouse was in the shape of a U. I wanted to add just a little extra support but I didn't want to get it in the way of the roof or the camera. I found some scraps from my previous cuts and created a mortise and tenon joint to hold them into place.

BZ2 7457
BZ2 7458

This was my first time doing this kind of joint and while it wasn't perfect, using cedar did make things easier because the wood is so soft and easy to cut with a chisel.

BZ2 7459

I used a vice to help cut the tops of the tenon side of the joint and then used a drill press and chisel for the mortise.

BZ2 7460

Hello, Benchy

At this point in the project I had been thinking about different ways to mount the camera onto the roof piece. I originally planned something out with a series of wooden pieces, but I wanted to make it easy to take the camera out if for some reason it could no longer be of use.

This was one of many little problems I wanted to solve in projects like this and it just so happened that I work with lots of folks who are into 3D printing. Our discussions over the years have had me interested in getting one, but they always seemed like they require a lot of work to get it up and running and maintain it over time.

I discussed the current state of 3D printers with a few people who have had experience and started to look into Prusa’s MK3 kit, but a friend of mine redirected me to the relatively new Bambu P1P. The P1P was a little bit cheaper than the MK3, but it promised faster printing speed and it seemed to be a little more accessible to 3D printing newbies like myself.

I took a chance and ordered the P1P—on the day before the similar Prusa MK4 was announced—along with a bunch of random PLA Basic and PLA Matte colors. In about a week the printer had arrived and with very little effort I had it set up and in about 20 minutes I had printed my first Benchy.

IMG 2133

While I was waiting on the printer to arrive I looked into a couple of apps, Bambu Slicer and Fusion360. Fusion360 has been in the maker community for years and I had first heard about it for making woodworking plans. On the one hand it is very accurate and using things like User Parameters sounded like a great way to plan out woodworking projects, however, I found that I could get by with some simple sketches for now.

For 3D printing, however, that accuracy is important and Fusion360 is an amazing tool. If you search for Fusion360 tutorials on YouTube, this video is likely to come up—for good reason because it does a really great job of going over the basics: