Skip to content

Instantly share code, notes, and snippets.

@librasteve
Created January 7, 2026 15:59
Show Gist options
  • Select an option

  • Save librasteve/a49e394fee09a9e91ed7b05f5ae6e6a0 to your computer and use it in GitHub Desktop.

Select an option

Save librasteve/a49e394fee09a9e91ed7b05f5ae6e6a0 to your computer and use it in GitHub Desktop.
Lightbox Example
#`[
Here's how to make an Air custom component, based on the Air::Base::Elements::Lightbox as an example
https://github.com/librasteve/Air/blob/4f52315079c1acb78607289920f7e4d009c8e816/lib/Air/Base/Elements.rakumod#L528
The key tools to do this are:
- inherit from Air::Component (class XX is Component or role YY does Component)
- method MM is controller {} (the trait will make a route ... the example in https://harcstack.org shows this)
- method HTML {} to return the HTML (required)
- method SCRIPT, SCRIPT-HEAD, CSS, SCSS (return JavaScript and CSS as needed - see Lightbox example below)
Deply your component as follows:
my $xx = $XX.new; #of course you can pass attrs here (each instance gets it's own serial)
my $site =
site :register($xx)
page
main
$xx;
$site.serve;
This is documented here https://librasteve.github.io/Air/docs/Air/Component.html (needs work)
Try LightBox in action by:
zef install Air::Examples
./bin/07-baseexamples.raku
#]
unit module MyComponents;
use Air::Functional :BASE-TAGS;
use Air::Component;
use Air::Base::Tags;
=head3 role Lightbox does Component is export
role Lightbox does Component is export {
has $!loaded;
#| unique lightbox label
has Str $.label = 'open';
has Button $.button;
#| can be provided with attrs
has %.attrs is rw;
#| can be provided with inners
has @.inners;
#| ok to call .new with @inners as Positional
multi method new(*@inners, *%attrs) {
self.bless: :@inners, :%attrs
}
method HTML {
if @!inners[0] ~~ Button && ! $!loaded++ {
$!button = @!inners.shift;
}
div [
if $!button {
a :href<#>, :class<open-link>, :data-target("#$.html-id"), $!button;
} else {
a :href<#>, :class<open-link>, :data-target("#$.html-id"), $!label;
}
div :class<lightbox-overlay>, :id($.html-id), [
div :class<lightbox-content>, [
span :class<close-btn>, Safe.new: '&times';
do-regular-tag( 'div', @.inners, |%.attrs )
];
];
];
}
method STYLE {
q:to/END/;
.lightbox-overlay {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 900;
}
.lightbox-overlay.active {
display: flex;
}
.lightbox-content {
background: grey;
width: 70vw;
position: relative;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
padding: 1rem;
}
.close-btn {
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
color: #333;
cursor: pointer;
}
END
}
method SCRIPT {
q:to/END/;
// Open specific lightbox
document.querySelectorAll('.open-link').forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
const target = document.querySelector(link.dataset.target);
if (target) target.classList.add('active');
});
});
// Close when clicking the X or outside the content
document.querySelectorAll('.lightbox-overlay').forEach(lightbox => {
const content = lightbox.querySelector('.lightbox-content');
const closeBtn = lightbox.querySelector('.close-btn');
closeBtn.addEventListener('click', () => {
lightbox.classList.remove('active');
});
lightbox.addEventListener('click', e => {
if (!content.contains(e.target)) {
lightbox.classList.remove('active');
}
});
});
// Close any open lightbox on Escape
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
document.querySelectorAll('.lightbox-overlay.active').forEach(lb => {
lb.classList.remove('active');
});
}
});
END
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment