Created
January 7, 2026 15:59
-
-
Save librasteve/a49e394fee09a9e91ed7b05f5ae6e6a0 to your computer and use it in GitHub Desktop.
Lightbox Example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #`[ | |
| 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: '×'; | |
| 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