@dodlhuat/basix 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -5
- package/css/lightbox.scss +272 -0
- package/css/style.css +256 -0
- package/css/style.css.map +1 -1
- package/css/style.scss +1 -0
- package/js/bottom-sheet.js +4 -3
- package/js/bottom-sheet.ts +5 -3
- package/js/calendar.js +9 -5
- package/js/calendar.ts +7 -2
- package/js/carousel.js +15 -11
- package/js/carousel.ts +16 -11
- package/js/chart.js +4 -4
- package/js/chart.ts +5 -3
- package/js/datepicker.js +11 -3
- package/js/datepicker.ts +13 -3
- package/js/docs-nav.js +1 -0
- package/js/editor.js +28 -20
- package/js/editor.ts +28 -20
- package/js/file-uploader.js +6 -10
- package/js/file-uploader.ts +7 -11
- package/js/flyout-menu.js +8 -2
- package/js/flyout-menu.ts +7 -2
- package/js/gallery.js +6 -13
- package/js/gallery.ts +8 -16
- package/js/group-picker.js +10 -7
- package/js/group-picker.ts +11 -7
- package/js/lightbox.js +277 -0
- package/js/lightbox.ts +331 -0
- package/js/modal.js +5 -4
- package/js/modal.ts +6 -4
- package/js/popover.js +4 -2
- package/js/popover.ts +4 -2
- package/js/push-menu.js +3 -2
- package/js/push-menu.ts +4 -2
- package/js/scrollbar.js +31 -23
- package/js/scrollbar.ts +36 -26
- package/js/select.js +23 -9
- package/js/select.ts +29 -11
- package/js/stepper.js +5 -1
- package/js/stepper.ts +6 -1
- package/js/table.js +8 -3
- package/js/table.ts +9 -3
- package/js/timepicker.js +20 -21
- package/js/timepicker.ts +23 -21
- package/js/toast.js +3 -7
- package/js/toast.ts +4 -8
- package/js/tooltip.js +13 -4
- package/js/tooltip.ts +16 -4
- package/js/tree.js +4 -0
- package/js/tree.ts +5 -0
- package/js/utils.js +29 -1
- package/js/utils.ts +36 -1
- package/js/virtual-dropdown.js +4 -8
- package/js/virtual-dropdown.ts +5 -9
- package/package.json +1 -3
package/README.md
CHANGED
|
@@ -16,12 +16,23 @@ A demo can be found here: <a href="http://www.andibauer.at/basix/" target="_blan
|
|
|
16
16
|
Take a look at style.scss for a glimpse on a full import. reset, parameters, colors & defaults are mandatory, anything
|
|
17
17
|
else can be added as needed.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
your own css or include the existing full style.css (or min)
|
|
19
|
+
Include the stylesheet and import individual components as ES modules. There is no bundled entry point — only the components you use are loaded.
|
|
21
20
|
|
|
22
21
|
``` html
|
|
23
22
|
<link rel="stylesheet" href="css/style.css" type="text/css">
|
|
24
|
-
<script
|
|
23
|
+
<script type="module">
|
|
24
|
+
import { Chart } from './js/chart.js';
|
|
25
|
+
import { Modal } from './js/modal.js';
|
|
26
|
+
import { Stepper } from './js/stepper.js';
|
|
27
|
+
// … add only what you need
|
|
28
|
+
</script>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
When installed via npm, import from the package:
|
|
32
|
+
|
|
33
|
+
``` js
|
|
34
|
+
import { Chart } from '@dodlhuat/basix/js/chart.js';
|
|
35
|
+
import { Modal } from '@dodlhuat/basix/js/modal.js';
|
|
25
36
|
```
|
|
26
37
|
|
|
27
38
|
---
|
|
@@ -125,10 +136,20 @@ The Switch component creates styled toggle switches based on checkboxes.
|
|
|
125
136
|
|
|
126
137
|
### Range Slider
|
|
127
138
|
|
|
128
|
-
The Range Slider component creates a
|
|
139
|
+
The Range Slider component creates a styled slider with a CSS custom property `--range-fill` that tracks the current value for fill styling. Use the JS class to initialise fill tracking automatically.
|
|
129
140
|
|
|
130
141
|
``` html
|
|
131
|
-
<
|
|
142
|
+
<div class="range-slider">
|
|
143
|
+
<input type="range" min="1" max="100" value="50" />
|
|
144
|
+
</div>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
``` js
|
|
148
|
+
// Initialise a single slider
|
|
149
|
+
new RangeSlider(document.querySelector('.range-slider input'));
|
|
150
|
+
|
|
151
|
+
// Or initialise all sliders on the page at once
|
|
152
|
+
RangeSlider.initAll();
|
|
132
153
|
```
|
|
133
154
|
|
|
134
155
|
---
|
|
@@ -155,6 +176,37 @@ The Flyout Menu component creates slide-in navigation menus with nested submenus
|
|
|
155
176
|
| `footerText` | string | `'© 2025 Brand Inc.'` | Shown in the footer if enabled |
|
|
156
177
|
| `enableFooter` | boolean | `true` | Shows the menu footer |
|
|
157
178
|
|
|
179
|
+
### Popover
|
|
180
|
+
|
|
181
|
+
The Popover component attaches a floating content panel to any trigger element. Supports click and hover trigger modes, four placement directions, optional arrow, and stacks correctly when multiple popovers exist.
|
|
182
|
+
|
|
183
|
+
| Option | Type | Default | Description |
|
|
184
|
+
|---|---|---|---|
|
|
185
|
+
| `content` | string | — | HTML content rendered inside the popover |
|
|
186
|
+
| `placement` | string | `'top'` | Preferred placement: `'top'`, `'bottom'`, `'left'`, `'right'`, or `'auto'` |
|
|
187
|
+
| `align` | string | `'center'` | Alignment along the axis: `'start'`, `'center'`, `'end'` |
|
|
188
|
+
| `offset` | number | `8` | Distance in px between the trigger and the popover |
|
|
189
|
+
| `arrow` | boolean | `true` | Shows a directional arrow |
|
|
190
|
+
| `triggerMode` | string | `'click'` | `'click'` or `'hover'` |
|
|
191
|
+
| `closeOnOutsideClick` | boolean | `true` | Closes when clicking outside |
|
|
192
|
+
| `closeOnEscape` | boolean | `true` | Closes on Escape key |
|
|
193
|
+
| `className` | string | — | Extra CSS class on the popover element |
|
|
194
|
+
| `onOpen` | function | — | Callback fired when the popover opens |
|
|
195
|
+
| `onClose` | function | — | Callback fired when the popover closes |
|
|
196
|
+
|
|
197
|
+
``` js
|
|
198
|
+
const pop = new Popover('#my-trigger', {
|
|
199
|
+
content: '<p>Popover content</p>',
|
|
200
|
+
placement: 'bottom',
|
|
201
|
+
triggerMode: 'click',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
pop.open();
|
|
205
|
+
pop.close();
|
|
206
|
+
pop.toggle();
|
|
207
|
+
pop.destroy();
|
|
208
|
+
```
|
|
209
|
+
|
|
158
210
|
### Dropdown Menu
|
|
159
211
|
|
|
160
212
|
The Dropdown Menu allows to create multi-level dropdown menus with nested submenus. The menu fires custom events `CustomEvent<DropdownSelectDetail>` that can be listened to in order to react to user selections.
|
|
@@ -228,6 +280,48 @@ sheet.hide();
|
|
|
228
280
|
sheet.snapTo('full');
|
|
229
281
|
```
|
|
230
282
|
|
|
283
|
+
### Lightbox
|
|
284
|
+
|
|
285
|
+
The Lightbox component opens images in a fullscreen overlay with an optional gallery mode. Supports keyboard navigation (← →, Escape), touch swipe, click-to-zoom, adjacent image preloading, focus trap, and a static `bind()` method for declarative HTML wiring.
|
|
286
|
+
|
|
287
|
+
| Option | Type | Default | Description |
|
|
288
|
+
|---|---|---|---|
|
|
289
|
+
| `src` | string | — | Image URL for single-image mode |
|
|
290
|
+
| `alt` | string | — | Alt text for the image |
|
|
291
|
+
| `caption` | string | — | Optional caption below the image |
|
|
292
|
+
| `images` | LightboxImage[] | — | Array of `{ src, alt?, caption? }` for gallery mode |
|
|
293
|
+
| `startIndex` | number | `0` | Starting index when opening a gallery |
|
|
294
|
+
| `closeable` | boolean | `true` | Shows × button and enables backdrop/Escape dismissal |
|
|
295
|
+
| `onOpen` | function | — | Callback fired when the lightbox opens |
|
|
296
|
+
| `onClose` | function | — | Callback fired after the close animation completes |
|
|
297
|
+
|
|
298
|
+
``` js
|
|
299
|
+
import { Lightbox } from '@dodlhuat/basix/js/lightbox.js';
|
|
300
|
+
|
|
301
|
+
// Single image
|
|
302
|
+
new Lightbox({ src: 'photo.jpg', alt: 'A landscape', caption: 'Taken at sunrise' }).show();
|
|
303
|
+
|
|
304
|
+
// Gallery
|
|
305
|
+
new Lightbox({
|
|
306
|
+
images: [
|
|
307
|
+
{ src: 'photo1.jpg', alt: 'Photo 1', caption: 'Day one' },
|
|
308
|
+
{ src: 'photo2.jpg', alt: 'Photo 2' },
|
|
309
|
+
],
|
|
310
|
+
startIndex: 0,
|
|
311
|
+
onClose: () => console.log('closed'),
|
|
312
|
+
}).show();
|
|
313
|
+
|
|
314
|
+
// Declarative binding — groups elements by data-lightbox value into galleries
|
|
315
|
+
Lightbox.bind();
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
``` html
|
|
319
|
+
<!-- Declarative usage -->
|
|
320
|
+
<a href="full.jpg" data-lightbox="trip" data-lightbox-caption="Arrival day">
|
|
321
|
+
<img src="thumb.jpg" alt="Arrival" />
|
|
322
|
+
</a>
|
|
323
|
+
```
|
|
324
|
+
|
|
231
325
|
### Tooltip
|
|
232
326
|
|
|
233
327
|
The Tooltip component shows contextual information on hover.
|
|
@@ -426,6 +520,91 @@ The Placeholder component creates skeleton loading states. Use `.placeholder` wi
|
|
|
426
520
|
|
|
427
521
|
## Advanced Components
|
|
428
522
|
|
|
523
|
+
### Chart
|
|
524
|
+
|
|
525
|
+
The Chart component renders SVG-based charts with no external dependencies. Supports line, area, column, bar, and pie chart types. Animates on first render and redraws on container resize.
|
|
526
|
+
|
|
527
|
+
``` js
|
|
528
|
+
const chart = new Chart('#chart-container', {
|
|
529
|
+
type: 'line',
|
|
530
|
+
title: 'Monthly Revenue',
|
|
531
|
+
series: [
|
|
532
|
+
{
|
|
533
|
+
name: 'Product A',
|
|
534
|
+
data: [
|
|
535
|
+
{ label: 'Jan', value: 120 },
|
|
536
|
+
{ label: 'Feb', value: 180 },
|
|
537
|
+
{ label: 'Mar', value: 150 },
|
|
538
|
+
],
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
chart.update(newSeries); // replace data and redraw
|
|
544
|
+
chart.setType('bar'); // switch chart type
|
|
545
|
+
chart.destroy(); // remove listeners and DOM
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
| Option | Type | Default | Description |
|
|
549
|
+
|---|---|---|---|
|
|
550
|
+
| `type` | ChartType | — | `'line'`, `'area'`, `'column'`, `'bar'`, or `'pie'` |
|
|
551
|
+
| `series` | ChartSeries[] | — | Array of `{ name, data, color? }` objects |
|
|
552
|
+
| `title` | string | — | Optional chart title |
|
|
553
|
+
| `subtitle` | string | — | Optional subtitle below the title |
|
|
554
|
+
| `height` | number | `280` | Inner chart height in px |
|
|
555
|
+
| `showLegend` | boolean | `true` | Renders the series legend |
|
|
556
|
+
| `showGrid` | boolean | `true` | Renders background grid lines |
|
|
557
|
+
| `animate` | boolean | `true` | Animates on first render |
|
|
558
|
+
| `curve` | string | `'smooth'` | Line interpolation for line/area: `'smooth'`, `'linear'`, `'step'` |
|
|
559
|
+
| `yMin` | number | `0` | Fixed y-axis minimum |
|
|
560
|
+
| `yMax` | number | auto | Fixed y-axis maximum (defaults to max value × 1.1) |
|
|
561
|
+
| `onPointClick` | function | — | Callback `(series, point, index) => void` fired on data point click |
|
|
562
|
+
|
|
563
|
+
### Calendar
|
|
564
|
+
|
|
565
|
+
The Calendar component renders a full interactive calendar with month, week, and agenda views. Supports event display, keyboard navigation, and locale configuration.
|
|
566
|
+
|
|
567
|
+
``` js
|
|
568
|
+
const cal = new Calendar({
|
|
569
|
+
container: '#my-calendar',
|
|
570
|
+
view: 'month',
|
|
571
|
+
events: [
|
|
572
|
+
{
|
|
573
|
+
id: '1',
|
|
574
|
+
title: 'Team Meeting',
|
|
575
|
+
start: new Date(2026, 3, 20, 10, 0),
|
|
576
|
+
end: new Date(2026, 3, 20, 11, 0),
|
|
577
|
+
className: 'badge-success',
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
onEventClick: (event) => console.log(event),
|
|
581
|
+
onDayClick: (date) => console.log(date),
|
|
582
|
+
onChange: (date, view) => console.log(date, view),
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
cal.next();
|
|
586
|
+
cal.prev();
|
|
587
|
+
cal.today();
|
|
588
|
+
cal.setView('week');
|
|
589
|
+
cal.addEvent({ id: '2', title: 'Lunch', start: new Date(), end: new Date() });
|
|
590
|
+
cal.removeEvent('2');
|
|
591
|
+
cal.setEvents(events);
|
|
592
|
+
cal.getEvents();
|
|
593
|
+
cal.destroy();
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
| Option | Type | Default | Description |
|
|
597
|
+
|---|---|---|---|
|
|
598
|
+
| `container` | HTMLElement \| string | — | Target container element or CSS selector |
|
|
599
|
+
| `events` | CalendarEvent[] | `[]` | Initial events |
|
|
600
|
+
| `view` | string | `'month'` | Initial view: `'month'`, `'week'`, or `'agenda'` |
|
|
601
|
+
| `showOutsideDays` | boolean | `true` | Show days from adjacent months in the month grid |
|
|
602
|
+
| `locale` | object | — | Override locale strings (day names, month names, labels) |
|
|
603
|
+
| `onDayClick` | function | — | Callback `(date: Date) => void` |
|
|
604
|
+
| `onEventClick` | function | — | Callback `(event: CalendarEvent) => void` |
|
|
605
|
+
| `onChange` | function | — | Callback `(date: Date, view: CalendarView) => void` |
|
|
606
|
+
| `className` | string | — | Extra CSS class on the root element |
|
|
607
|
+
|
|
429
608
|
### Context Menu
|
|
430
609
|
|
|
431
610
|
The Context Menu component shows a custom right-click menu on any target element. Supports icons, keyboard shortcuts, group labels, separators, submenus, destructive items, and disabled items. Automatically flips to avoid viewport overflow and animates in from the click origin.
|
|
@@ -594,6 +773,74 @@ const picker = new GroupPicker('#group-picker-demo', data, {
|
|
|
594
773
|
| `collapseAll()` | Collapse all groups |
|
|
595
774
|
| `destroy()` | Remove event listeners and clear the DOM |
|
|
596
775
|
|
|
776
|
+
### Time Span Picker
|
|
777
|
+
|
|
778
|
+
The TimeSpanPicker component provides a paired start/end time input for selecting a time range.
|
|
779
|
+
|
|
780
|
+
``` js
|
|
781
|
+
const picker = new TimeSpanPicker('my-container', {
|
|
782
|
+
defaultStart: '09:00',
|
|
783
|
+
defaultEnd: '17:00',
|
|
784
|
+
onChange: (start, end) => console.log(start, end),
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
picker.getValue(); // { start: '09:00', end: '17:00' }
|
|
788
|
+
picker.setValue('10:00', '18:00');
|
|
789
|
+
picker.reset();
|
|
790
|
+
picker.isValid(); // boolean
|
|
791
|
+
picker.destroy();
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Select
|
|
795
|
+
|
|
796
|
+
The Select component wraps a native `<select>` element with custom Basix styling. Supports single and multi-select.
|
|
797
|
+
|
|
798
|
+
``` html
|
|
799
|
+
<select id="my-select">
|
|
800
|
+
<option value="a">Option A</option>
|
|
801
|
+
<option value="b">Option B</option>
|
|
802
|
+
</select>
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
``` js
|
|
806
|
+
const sel = new Select('#my-select');
|
|
807
|
+
sel.value(); // returns selected value string, or string[] for multi-select
|
|
808
|
+
|
|
809
|
+
// Or initialise by passing the element directly
|
|
810
|
+
Select.init(document.querySelector('#my-select'));
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### Code Viewer
|
|
814
|
+
|
|
815
|
+
The CodeViewer component renders syntax-highlighted code blocks inside any container. Supports JavaScript, HTML, and CSS.
|
|
816
|
+
|
|
817
|
+
``` js
|
|
818
|
+
new CodeViewer('#output', '<div class="card">Hello</div>', 'html');
|
|
819
|
+
new CodeViewer('#output', 'const x = 42;', 'javascript');
|
|
820
|
+
new CodeViewer('#output', '.card { padding: 1rem; }', 'css');
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### Editor
|
|
824
|
+
|
|
825
|
+
The Editor component provides a contenteditable rich-text editing area with undo/redo, word count, and an optional side panel showing the raw HTML source and a live preview. Requires a `#editable` element in the DOM.
|
|
826
|
+
|
|
827
|
+
``` html
|
|
828
|
+
<div id="editable" contenteditable="true"></div>
|
|
829
|
+
<!-- Optional side panel elements -->
|
|
830
|
+
<textarea id="code"></textarea>
|
|
831
|
+
<div id="preview"></div>
|
|
832
|
+
<div id="sidePanel"></div>
|
|
833
|
+
<span id="wordCount"></span>
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
``` js
|
|
837
|
+
// Full editor with side panel
|
|
838
|
+
new Editor();
|
|
839
|
+
|
|
840
|
+
// Simple mode — hides the side panel permanently
|
|
841
|
+
new Editor({ simple: true });
|
|
842
|
+
```
|
|
843
|
+
|
|
597
844
|
### Custom Scrollbar
|
|
598
845
|
|
|
599
846
|
The Scrollbar component creates custom-styled scrollbars. Supports pointer/touch dragging, track clicking, and automatic thumb sizing. Can be used with any class.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
@use "properties";
|
|
2
|
+
@use "parameters" as *;
|
|
3
|
+
|
|
4
|
+
.lightbox-wrapper {
|
|
5
|
+
position: fixed;
|
|
6
|
+
inset: 0;
|
|
7
|
+
z-index: 999;
|
|
8
|
+
pointer-events: none;
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
|
|
13
|
+
&.is-visible {
|
|
14
|
+
pointer-events: auto;
|
|
15
|
+
|
|
16
|
+
.lightbox-background {
|
|
17
|
+
opacity: 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.lightbox {
|
|
21
|
+
opacity: 1;
|
|
22
|
+
transform: scale(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.lightbox-close,
|
|
26
|
+
.lightbox-prev,
|
|
27
|
+
.lightbox-next {
|
|
28
|
+
opacity: 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.lightbox-background {
|
|
34
|
+
position: absolute;
|
|
35
|
+
inset: 0;
|
|
36
|
+
background: rgba(0, 0, 0, 0.88);
|
|
37
|
+
opacity: 0;
|
|
38
|
+
backdrop-filter: blur(8px);
|
|
39
|
+
-webkit-backdrop-filter: blur(8px);
|
|
40
|
+
transition: opacity 0.3s ease;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.lightbox-close {
|
|
44
|
+
position: absolute;
|
|
45
|
+
top: $spacing;
|
|
46
|
+
right: $spacing;
|
|
47
|
+
z-index: 1002;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
width: 2.25rem;
|
|
52
|
+
height: 2.25rem;
|
|
53
|
+
border: none;
|
|
54
|
+
border-radius: 50%;
|
|
55
|
+
background: rgba(255, 255, 255, 0.12);
|
|
56
|
+
color: rgba(255, 255, 255, 0.8);
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
opacity: 0;
|
|
59
|
+
transition: background 0.2s ease, color 0.2s ease, opacity 0.3s ease;
|
|
60
|
+
-webkit-tap-highlight-color: transparent;
|
|
61
|
+
|
|
62
|
+
.icon {
|
|
63
|
+
font-size: 1.25rem;
|
|
64
|
+
line-height: 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&:hover {
|
|
68
|
+
background: rgba(255, 255, 255, 0.22);
|
|
69
|
+
color: #fff;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&:focus-visible {
|
|
73
|
+
outline: 2px solid rgba(255, 255, 255, 0.6);
|
|
74
|
+
outline-offset: 2px;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.lightbox {
|
|
79
|
+
position: relative;
|
|
80
|
+
z-index: 1000;
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
align-items: center;
|
|
84
|
+
max-width: min(90vw, 1280px);
|
|
85
|
+
opacity: 0;
|
|
86
|
+
transform: scale(0.97);
|
|
87
|
+
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.lightbox-img-wrap {
|
|
91
|
+
position: relative;
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
max-width: 100%;
|
|
96
|
+
max-height: calc(90dvh - 3.5rem);
|
|
97
|
+
cursor: zoom-in;
|
|
98
|
+
border-radius: calc($border-radius * 2);
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
background: rgba(255, 255, 255, 0.04);
|
|
101
|
+
|
|
102
|
+
&.is-loading {
|
|
103
|
+
min-width: 6rem;
|
|
104
|
+
min-height: 6rem;
|
|
105
|
+
|
|
106
|
+
.lightbox-img {
|
|
107
|
+
opacity: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.lightbox-spinner {
|
|
111
|
+
display: flex;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
&.is-error {
|
|
116
|
+
min-width: 14rem;
|
|
117
|
+
min-height: 8rem;
|
|
118
|
+
cursor: default;
|
|
119
|
+
|
|
120
|
+
.lightbox-img {
|
|
121
|
+
display: none;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.lightbox-spinner {
|
|
125
|
+
display: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
&::after {
|
|
129
|
+
content: 'Failed to load image';
|
|
130
|
+
color: rgba(255, 255, 255, 0.4);
|
|
131
|
+
font-size: 0.875rem;
|
|
132
|
+
padding: $spacing * 2;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
&.is-zoomed {
|
|
137
|
+
cursor: zoom-out;
|
|
138
|
+
overflow: auto;
|
|
139
|
+
max-height: 90dvh;
|
|
140
|
+
border-radius: 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.lightbox-spinner {
|
|
145
|
+
position: absolute;
|
|
146
|
+
inset: 0;
|
|
147
|
+
display: none;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
z-index: 1;
|
|
151
|
+
|
|
152
|
+
// Override spinner colors for the dark overlay
|
|
153
|
+
.spinner {
|
|
154
|
+
border-color: rgba(255, 255, 255, 0.15);
|
|
155
|
+
border-top-color: rgba(255, 255, 255, 0.7);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.lightbox-img {
|
|
160
|
+
display: block;
|
|
161
|
+
max-width: 100%;
|
|
162
|
+
max-height: calc(90dvh - 3.5rem);
|
|
163
|
+
object-fit: contain;
|
|
164
|
+
opacity: 0;
|
|
165
|
+
transition: opacity 0.25s ease;
|
|
166
|
+
border-radius: calc($border-radius * 2);
|
|
167
|
+
user-select: none;
|
|
168
|
+
-webkit-user-drag: none;
|
|
169
|
+
|
|
170
|
+
&.is-loaded {
|
|
171
|
+
opacity: 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
&.is-zoomed {
|
|
175
|
+
max-width: none;
|
|
176
|
+
max-height: none;
|
|
177
|
+
border-radius: 0;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.lightbox-caption {
|
|
182
|
+
margin: calc($spacing * 0.6) 0 0;
|
|
183
|
+
color: rgba(255, 255, 255, 0.6);
|
|
184
|
+
font-size: 0.875rem;
|
|
185
|
+
text-align: center;
|
|
186
|
+
max-width: 60ch;
|
|
187
|
+
line-height: 1.5;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.lightbox-counter {
|
|
191
|
+
margin-top: calc($spacing * 0.5);
|
|
192
|
+
color: rgba(255, 255, 255, 0.4);
|
|
193
|
+
font-size: 0.75rem;
|
|
194
|
+
letter-spacing: 0.04em;
|
|
195
|
+
text-align: center;
|
|
196
|
+
user-select: none;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Prev/Next buttons — glass pill, matches carousel style
|
|
200
|
+
.lightbox-prev,
|
|
201
|
+
.lightbox-next {
|
|
202
|
+
position: fixed;
|
|
203
|
+
top: 50%;
|
|
204
|
+
transform: translateY(-50%);
|
|
205
|
+
z-index: 1001;
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
justify-content: center;
|
|
209
|
+
width: 2.5rem;
|
|
210
|
+
height: 2.5rem;
|
|
211
|
+
border: none;
|
|
212
|
+
border-radius: 50%;
|
|
213
|
+
background: rgba(0, 0, 0, 0.32);
|
|
214
|
+
backdrop-filter: blur(8px);
|
|
215
|
+
-webkit-backdrop-filter: blur(8px);
|
|
216
|
+
color: rgba(255, 255, 255, 0.92);
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
opacity: 0;
|
|
219
|
+
transition: background 0.2s ease,
|
|
220
|
+
opacity 0.3s ease,
|
|
221
|
+
transform 0.2s ease;
|
|
222
|
+
-webkit-tap-highlight-color: transparent;
|
|
223
|
+
|
|
224
|
+
.icon {
|
|
225
|
+
font-size: 1.5rem;
|
|
226
|
+
line-height: 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
&:hover {
|
|
230
|
+
background: rgba(0, 0, 0, 0.55);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
&:active {
|
|
234
|
+
transform: translateY(-50%) scale(0.93);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
&:focus-visible {
|
|
238
|
+
outline: 2px solid rgba(255, 255, 255, 0.6);
|
|
239
|
+
outline-offset: 2px;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.lightbox-prev { left: $spacing; }
|
|
244
|
+
.lightbox-next { right: $spacing; }
|
|
245
|
+
|
|
246
|
+
@media (max-width: 768px) {
|
|
247
|
+
.lightbox-prev,
|
|
248
|
+
.lightbox-next {
|
|
249
|
+
width: 2rem;
|
|
250
|
+
height: 2rem;
|
|
251
|
+
|
|
252
|
+
.icon { font-size: 1.25rem; }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.lightbox-prev { left: calc($spacing * 0.5); }
|
|
256
|
+
.lightbox-next { right: calc($spacing * 0.5); }
|
|
257
|
+
|
|
258
|
+
.lightbox {
|
|
259
|
+
max-width: 100vw;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.lightbox-img,
|
|
263
|
+
.lightbox-img-wrap {
|
|
264
|
+
max-height: 82dvh;
|
|
265
|
+
border-radius: $border-radius;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.lightbox-close {
|
|
269
|
+
top: calc($spacing * 0.75);
|
|
270
|
+
right: calc($spacing * 0.75);
|
|
271
|
+
}
|
|
272
|
+
}
|