elvish-css 1.0.0

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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +518 -0
  3. package/dist/elvish.css +2194 -0
  4. package/dist/elvish.d.ts +78 -0
  5. package/dist/elvish.esm.js +2185 -0
  6. package/dist/elvish.iife.js +2169 -0
  7. package/dist/elvish.min.css +9 -0
  8. package/dist/elvish.umd.js +2173 -0
  9. package/elvish.css +28 -0
  10. package/elvish.js +81 -0
  11. package/global/global.css +16 -0
  12. package/global/modern.css +305 -0
  13. package/global/reset.css +507 -0
  14. package/global/tokens.css +154 -0
  15. package/global/transitions.css +288 -0
  16. package/global/transitions.js +289 -0
  17. package/global/utilities.css +151 -0
  18. package/package.json +61 -0
  19. package/primitives/adleithian/adleithian.css +16 -0
  20. package/primitives/adleithian/adleithian.js +63 -0
  21. package/primitives/bau/bau.css +86 -0
  22. package/primitives/bau/bau.js +127 -0
  23. package/primitives/enedh/enedh.css +38 -0
  24. package/primitives/enedh/enedh.js +110 -0
  25. package/primitives/esgal/esgal.css +39 -0
  26. package/primitives/esgal/esgal.js +115 -0
  27. package/primitives/fano/fano.css +28 -0
  28. package/primitives/fano/fano.js +108 -0
  29. package/primitives/gant-thala/gant-thala.css +32 -0
  30. package/primitives/gant-thala/gant-thala.js +69 -0
  31. package/primitives/glan-tholl/glan-tholl.css +71 -0
  32. package/primitives/glan-tholl/glan-tholl.js +147 -0
  33. package/primitives/glan-veleg/glan-veleg.css +45 -0
  34. package/primitives/glan-veleg/glan-veleg.js +138 -0
  35. package/primitives/gonath/gonath.css +57 -0
  36. package/primitives/gonath/gonath.js +113 -0
  37. package/primitives/gwistindor/gwistindor.css +52 -0
  38. package/primitives/gwistindor/gwistindor.js +96 -0
  39. package/primitives/hath/hath.css +39 -0
  40. package/primitives/hath/hath.js +107 -0
  41. package/primitives/him/him.css +43 -0
  42. package/primitives/him/him.js +169 -0
  43. package/primitives/miriant/miriant.css +75 -0
  44. package/primitives/miriant/miriant.js +158 -0
  45. package/primitives/thann/thann.css +57 -0
  46. package/primitives/thann/thann.js +96 -0
  47. package/primitives/tiniath/tiniath.css +16 -0
  48. package/primitives/tiniath/tiniath.js +88 -0
  49. package/primitives/vircantie/vircantie.css +24 -0
  50. package/primitives/vircantie/vircantie.js +83 -0
@@ -0,0 +1,2169 @@
1
+ /**
2
+ * Elvish CSS Layout System v1.0.0
3
+ * Custom Elements for intrinsic CSS layouts
4
+ *
5
+ * https://github.com/star-this/elvish-css
6
+ * License: MIT
7
+ */
8
+
9
+
10
+ (function() {
11
+ 'use strict';
12
+
13
+ // Transitions
14
+ /**
15
+ * Elvish - View Transitions Helper
16
+ *
17
+ * Utilities for smooth view transitions between layout states.
18
+ *
19
+ * Usage:
20
+ *
21
+ * import { transition, transitionTo } from './transitions.js';
22
+ *
23
+ * // Wrap any DOM change in a transition
24
+ * transition(() => {
25
+ * element.classList.toggle('collapsed');
26
+ * });
27
+ *
28
+ * // With custom options
29
+ * transition(() => {
30
+ * sidebar.hidden = true;
31
+ * }, { duration: 500 });
32
+ *
33
+ * // Transition with callback after completion
34
+ * transition(() => {
35
+ * grid.setAttribute('columns', '4');
36
+ * }).then(() => {
37
+ * console.log('Transition complete!');
38
+ * });
39
+ */
40
+
41
+ /**
42
+ * Check if View Transitions API is supported
43
+ */
44
+ var supportsViewTransitions = () =>
45
+ typeof document !== 'undefined' &&
46
+ 'startViewTransition' in document;
47
+
48
+ /**
49
+ * Get the currently active view transition (if any)
50
+ * @returns {ViewTransition|null}
51
+ */
52
+ var getActiveTransition = () =>
53
+ document.activeViewTransition ?? null;
54
+
55
+ /**
56
+ * Wrap a DOM mutation in a view transition
57
+ *
58
+ * @param {Function} updateCallback - Function that performs DOM changes
59
+ * @param {Object} options - Configuration options
60
+ * @param {number} options.duration - Override transition duration (ms)
61
+ * @param {string} options.easing - Override easing function
62
+ * @param {string[]} options.types - Transition types for :active-view-transition-type()
63
+ * @returns {Promise} Resolves when transition completes
64
+ */
65
+ function transition(updateCallback, options = {}) {
66
+ // If View Transitions not supported, just run the callback
67
+ if (!supportsViewTransitions()) {
68
+ updateCallback();
69
+ return Promise.resolve();
70
+ }
71
+
72
+ // Apply custom duration/easing if provided
73
+ var root = document.documentElement;
74
+ var originalDuration = root.style.getPropertyValue('--transition-duration');
75
+ var originalEasing = root.style.getPropertyValue('--transition-ease');
76
+
77
+ if (options.duration) {
78
+ root.style.setProperty('--transition-duration', `${options.duration}ms`);
79
+ }
80
+ if (options.easing) {
81
+ root.style.setProperty('--transition-ease', options.easing);
82
+ }
83
+
84
+ // Start the view transition with optional types
85
+ var transitionOptions = options.types
86
+ ? { update: updateCallback, types: options.types }
87
+ : updateCallback;
88
+
89
+ var viewTransition = document.startViewTransition(transitionOptions);
90
+
91
+ // Restore original values after transition
92
+ return viewTransition.finished.then(() => {
93
+ if (options.duration) {
94
+ root.style.setProperty('--transition-duration', originalDuration || '');
95
+ }
96
+ if (options.easing) {
97
+ root.style.setProperty('--transition-ease', originalEasing || '');
98
+ }
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Transition an element to a new state by changing attributes/classes
104
+ *
105
+ * @param {HTMLElement} element - Element to transition
106
+ * @param {Object} changes - Attribute/class changes to apply
107
+ * @param {Object} changes.attrs - Attributes to set (use null to remove)
108
+ * @param {string[]} changes.addClass - Classes to add
109
+ * @param {string[]} changes.removeClass - Classes to remove
110
+ * @param {string[]} changes.toggleClass - Classes to toggle
111
+ * @param {Object} changes.style - Inline styles to set
112
+ * @param {Object} options - Transition options
113
+ * @returns {Promise}
114
+ */
115
+ function transitionTo(element, changes = {}, options = {}) {
116
+ return transition(() => {
117
+ // Apply attribute changes
118
+ if (changes.attrs) {
119
+ Object.entries(changes.attrs).forEach(([key, value]) => {
120
+ if (value === null) {
121
+ element.removeAttribute(key);
122
+ } else {
123
+ element.setAttribute(key, value);
124
+ }
125
+ });
126
+ }
127
+
128
+ // Apply class changes
129
+ if (changes.addClass) {
130
+ element.classList.add(...changes.addClass);
131
+ }
132
+ if (changes.removeClass) {
133
+ element.classList.remove(...changes.removeClass);
134
+ }
135
+ if (changes.toggleClass) {
136
+ changes.toggleClass.forEach(cls => element.classList.toggle(cls));
137
+ }
138
+
139
+ // Apply style changes
140
+ if (changes.style) {
141
+ Object.entries(changes.style).forEach(([prop, value]) => {
142
+ element.style.setProperty(prop, value);
143
+ });
144
+ }
145
+ }, options);
146
+ }
147
+
148
+ /**
149
+ * Transition between two elements (e.g., swap content)
150
+ *
151
+ * @param {HTMLElement} outElement - Element leaving
152
+ * @param {HTMLElement} inElement - Element entering
153
+ * @param {Object} options - Transition options
154
+ * @returns {Promise}
155
+ */
156
+ function crossfade(outElement, inElement, options = {}) {
157
+ return transition(() => {
158
+ outElement.hidden = true;
159
+ inElement.hidden = false;
160
+ }, options);
161
+ }
162
+
163
+ /**
164
+ * Create a named transition group for an element
165
+ * Elements with the same view-transition-name animate together
166
+ *
167
+ * @param {HTMLElement} element - Element to name
168
+ * @param {string} name - Transition name
169
+ */
170
+ function setTransitionName(element, name) {
171
+ element.style.viewTransitionName = name;
172
+ }
173
+
174
+ /**
175
+ * Remove transition name from element
176
+ *
177
+ * @param {HTMLElement} element
178
+ */
179
+ function clearTransitionName(element) {
180
+ element.style.viewTransitionName = '';
181
+ }
182
+
183
+ /**
184
+ * Batch multiple elements with unique transition names
185
+ * Useful for list/grid items that should animate independently
186
+ *
187
+ * @param {NodeList|HTMLElement[]} elements - Elements to name
188
+ * @param {string} prefix - Name prefix (will add index)
189
+ */
190
+ function nameTransitionGroup(elements, prefix = 'item') {
191
+ elements.forEach((el, i) => {
192
+ el.style.viewTransitionName = `${prefix}-${i}`;
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Enable auto-naming with match-element for a container's children
198
+ * Each child gets a unique browser-generated name automatically
199
+ *
200
+ * @param {HTMLElement} container - Parent element
201
+ * @param {string} [transitionClass] - Optional view-transition-class for group styling
202
+ */
203
+ function enableAutoNaming(container, transitionClass) {
204
+ Array.from(container.children).forEach(child => {
205
+ child.style.viewTransitionName = 'match-element';
206
+ if (transitionClass) {
207
+ child.style.viewTransitionClass = transitionClass;
208
+ }
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Disable auto-naming for a container's children
214
+ *
215
+ * @param {HTMLElement} container - Parent element
216
+ */
217
+ function disableAutoNaming(container) {
218
+ Array.from(container.children).forEach(child => {
219
+ child.style.viewTransitionName = '';
220
+ child.style.viewTransitionClass = '';
221
+ });
222
+ }
223
+
224
+ /**
225
+ * Elvish-specific: Transition ratio change
226
+ *
227
+ * @param {'golden'|'silver'|'fifth'} ratio - New ratio
228
+ * @param {Object} options - Transition options
229
+ */
230
+ function transitionRatio(ratio, options = {}) {
231
+ var ratioVar = `var(--ratio-${ratio})`;
232
+ var measures = { golden: '60ch', fifth: '70ch', silver: '80ch' };
233
+
234
+ return transition(() => {
235
+ var root = document.documentElement;
236
+ root.style.setProperty('--ratio', ratioVar);
237
+ root.style.setProperty('--measure', measures[ratio]);
238
+ root.dataset.ratio = ratio;
239
+ }, { duration: 400, types: ['layout', 'ratio'], ...options });
240
+ }
241
+
242
+ /**
243
+ * Elvish-specific: Transition theme change
244
+ *
245
+ * @param {'light'|'dark'|'auto'} theme - New theme
246
+ * @param {Object} options - Transition options
247
+ */
248
+ function transitionTheme(theme, options = {}) {
249
+ return transition(() => {
250
+ var root = document.documentElement;
251
+ if (theme === 'auto') {
252
+ root.style.colorScheme = 'light dark';
253
+ } else {
254
+ root.style.colorScheme = theme;
255
+ }
256
+ localStorage.setItem('elvish-theme', theme);
257
+ }, { duration: 300, types: ['theme'], ...options });
258
+ }
259
+
260
+ /**
261
+ * Elvish-specific: Transition layout primitive state
262
+ *
263
+ * @param {HTMLElement} primitive - Elvish layout element
264
+ * @param {Object} attrs - New attribute values
265
+ * @param {Object} options - Transition options
266
+ */
267
+ function transitionLayout(primitive, attrs, options = {}) {
268
+ // Give the primitive a transition name if it doesn't have one
269
+ if (!primitive.style.viewTransitionName) {
270
+ primitive.style.viewTransitionName = primitive.tagName.toLowerCase();
271
+ }
272
+
273
+ return transitionTo(primitive, { attrs }, { types: ['layout'], ...options });
274
+ }
275
+
276
+ /**
277
+ * Skip/cancel the currently active view transition
278
+ */
279
+ function skipTransition() {
280
+ var active = getActiveTransition();
281
+ if (active) {
282
+ active.skipTransition();
283
+ }
284
+ }
285
+
286
+ // Default export for convenient importing
287
+ default {
288
+ transition,
289
+ transitionTo,
290
+ crossfade,
291
+ setTransitionName,
292
+ clearTransitionName,
293
+ nameTransitionGroup,
294
+ enableAutoNaming,
295
+ disableAutoNaming,
296
+ transitionRatio,
297
+ transitionTheme,
298
+ transitionLayout,
299
+ skipTransition,
300
+ supportsViewTransitions,
301
+ getActiveTransition,
302
+ };
303
+
304
+
305
+ // Primitives (auto-register)
306
+ // hath
307
+ /**
308
+ * Stack Layout Custom Element
309
+ *
310
+ * Injects vertical margin between successive child elements.
311
+ *
312
+ * @property {string} space - A CSS margin value (default: var(--s1))
313
+ * @property {boolean} recursive - Apply to all descendants, not just children
314
+ * @property {number} splitAfter - Element index after which to split with auto margin
315
+ *
316
+ * @example
317
+ * <i-hath space="var(--s2)">
318
+ * <h2>Heading</h2>
319
+ * <p>Paragraph one</p>
320
+ * <p>Paragraph two</p>
321
+ * </i-hath>
322
+ */
323
+
324
+ class HathLayout extends HTMLElement {
325
+ static get observedAttributes() {
326
+ return ['space', 'recursive', 'split-after'];
327
+ }
328
+
329
+ constructor() {
330
+ super();
331
+ this.render = this.render.bind(this);
332
+ }
333
+
334
+ connectedCallback() {
335
+ this.render();
336
+ }
337
+
338
+ attributeChangedCallback() {
339
+ this.render();
340
+ }
341
+
342
+ get space() {
343
+ return this.getAttribute('space') || 'var(--s1)';
344
+ }
345
+
346
+ set space(val) {
347
+ this.setAttribute('space', val);
348
+ }
349
+
350
+ get recursive() {
351
+ return this.hasAttribute('recursive');
352
+ }
353
+
354
+ set recursive(val) {
355
+ if (val) {
356
+ this.setAttribute('recursive', '');
357
+ } else {
358
+ this.removeAttribute('recursive');
359
+ }
360
+ }
361
+
362
+ get splitAfter() {
363
+ const val = this.getAttribute('split-after');
364
+ return val ? parseInt(val, 10) : null;
365
+ }
366
+
367
+ set splitAfter(val) {
368
+ if (val) {
369
+ this.setAttribute('split-after', val);
370
+ } else {
371
+ this.removeAttribute('split-after');
372
+ }
373
+ }
374
+
375
+ render() {
376
+ // Generate unique identifier for this configuration
377
+ const recursive = this.recursive ? '-recursive' : '';
378
+ const split = this.splitAfter ? `-split${this.splitAfter}` : '';
379
+ const id = `Hath-${this.space}${recursive}${split}`.replace(/[^\w-]/g, '');
380
+
381
+ this.dataset.i = id;
382
+ this.style.setProperty('--hath-space', this.space);
383
+
384
+ // Check if styles already exist for this configuration
385
+ if (!document.getElementById(id)) {
386
+ const styleEl = document.createElement('style');
387
+ styleEl.id = id;
388
+
389
+ const selector = `[data-i="${id}"]`;
390
+ let css = '';
391
+
392
+ if (this.recursive) {
393
+ css = `${selector} * + * { margin-block-start: ${this.space}; }`;
394
+ } else {
395
+ css = `${selector} > * + * { margin-block-start: ${this.space}; }`;
396
+ }
397
+
398
+ if (this.splitAfter) {
399
+ css += `${selector} > :nth-child(${this.splitAfter}) { margin-block-end: auto; }`;
400
+ }
401
+
402
+ styleEl.textContent = css;
403
+ document.head.appendChild(styleEl);
404
+ }
405
+ }
406
+ }
407
+
408
+ // Define the custom element
409
+ if ('customElements' in window) {
410
+ customElements.define('i-hath', HathLayout);
411
+ }
412
+
413
+
414
+
415
+
416
+ // bau
417
+ /**
418
+ * Box Layout Custom Element
419
+ *
420
+ * A simple container with padding and optional border/background.
421
+ *
422
+ * @property {string} padding - A CSS padding value (default: var(--s1))
423
+ * @property {string} borderWidth - A CSS border-width value (default: var(--border-thin))
424
+ * @property {boolean} invert - Swap foreground and background colors
425
+ * @property {boolean} borderless - Remove border
426
+ * @property {boolean} compact - Remove padding
427
+ *
428
+ * @example
429
+ * <i-bau padding="var(--s2)" invert>
430
+ * <p>Content in a box</p>
431
+ * </i-bau>
432
+ */
433
+
434
+ class BauLayout extends HTMLElement {
435
+ static get observedAttributes() {
436
+ return ['padding', 'border-width', 'invert', 'borderless', 'compact'];
437
+ }
438
+
439
+ constructor() {
440
+ super();
441
+ this.render = this.render.bind(this);
442
+ }
443
+
444
+ connectedCallback() {
445
+ this.render();
446
+ }
447
+
448
+ attributeChangedCallback() {
449
+ this.render();
450
+ }
451
+
452
+ get padding() {
453
+ return this.getAttribute('padding') || 'var(--s1)';
454
+ }
455
+
456
+ set padding(val) {
457
+ this.setAttribute('padding', val);
458
+ }
459
+
460
+ get borderWidth() {
461
+ return this.getAttribute('border-width') || 'var(--border-thin)';
462
+ }
463
+
464
+ set borderWidth(val) {
465
+ this.setAttribute('border-width', val);
466
+ }
467
+
468
+ get invert() {
469
+ return this.hasAttribute('invert');
470
+ }
471
+
472
+ set invert(val) {
473
+ if (val) {
474
+ this.setAttribute('invert', '');
475
+ } else {
476
+ this.removeAttribute('invert');
477
+ }
478
+ }
479
+
480
+ get borderless() {
481
+ return this.hasAttribute('borderless');
482
+ }
483
+
484
+ set borderless(val) {
485
+ if (val) {
486
+ this.setAttribute('borderless', '');
487
+ } else {
488
+ this.removeAttribute('borderless');
489
+ }
490
+ }
491
+
492
+ get compact() {
493
+ return this.hasAttribute('compact');
494
+ }
495
+
496
+ set compact(val) {
497
+ if (val) {
498
+ this.setAttribute('compact', '');
499
+ } else {
500
+ this.removeAttribute('compact');
501
+ }
502
+ }
503
+
504
+ render() {
505
+ // Set CSS custom properties for this instance
506
+ this.style.setProperty('--bau-padding', this.padding);
507
+ this.style.setProperty('--bau-border-width', this.borderWidth);
508
+
509
+ // Generate unique identifier
510
+ const invertStr = this.invert ? '-invert' : '';
511
+ const borderlessStr = this.borderless ? '-borderless' : '';
512
+ const compactStr = this.compact ? '-compact' : '';
513
+ const id = `Bau-${this.padding}-${this.borderWidth}${invertStr}${borderlessStr}${compactStr}`.replace(/[^\w-]/g, '');
514
+
515
+ this.dataset.i = id;
516
+
517
+ // Check if styles already exist
518
+ if (!document.getElementById(id)) {
519
+ const styleEl = document.createElement('style');
520
+ styleEl.id = id;
521
+
522
+ const selector = `[data-i="${id}"]`;
523
+ let css = `${selector} { padding: ${this.padding}; border-width: ${this.borderWidth}; }`;
524
+
525
+ if (this.borderless) {
526
+ css += `${selector} { border-width: 0; }`;
527
+ }
528
+
529
+ if (this.compact) {
530
+ css += `${selector} { padding: 0; }`;
531
+ }
532
+
533
+ styleEl.textContent = css;
534
+ document.head.appendChild(styleEl);
535
+ }
536
+ }
537
+ }
538
+
539
+ if ('customElements' in window) {
540
+ customElements.define('i-bau', BauLayout);
541
+ }
542
+
543
+
544
+
545
+
546
+ // enedh
547
+ /**
548
+ * Center Layout Custom Element
549
+ *
550
+ * Horizontally centers content with a maximum width.
551
+ *
552
+ * @property {string} max - Maximum width (default: var(--measure))
553
+ * @property {string} gutters - Minimum space on sides (default: 0)
554
+ * @property {boolean} intrinsic - Center children based on their content width
555
+ * @property {boolean} andText - Also center text alignment
556
+ *
557
+ * @example
558
+ * <i-enedh max="40ch" gutters="var(--s1)">
559
+ * <p>Centered content</p>
560
+ * </i-enedh>
561
+ */
562
+
563
+ class EnedhLayout extends HTMLElement {
564
+ static get observedAttributes() {
565
+ return ['max', 'gutters', 'intrinsic', 'and-text'];
566
+ }
567
+
568
+ constructor() {
569
+ super();
570
+ this.render = this.render.bind(this);
571
+ }
572
+
573
+ connectedCallback() {
574
+ this.render();
575
+ }
576
+
577
+ attributeChangedCallback() {
578
+ this.render();
579
+ }
580
+
581
+ get max() {
582
+ return this.getAttribute('max') || 'var(--measure)';
583
+ }
584
+
585
+ set max(val) {
586
+ this.setAttribute('max', val);
587
+ }
588
+
589
+ get gutters() {
590
+ return this.getAttribute('gutters') || '0';
591
+ }
592
+
593
+ set gutters(val) {
594
+ this.setAttribute('gutters', val);
595
+ }
596
+
597
+ get intrinsic() {
598
+ return this.hasAttribute('intrinsic');
599
+ }
600
+
601
+ set intrinsic(val) {
602
+ if (val) {
603
+ this.setAttribute('intrinsic', '');
604
+ } else {
605
+ this.removeAttribute('intrinsic');
606
+ }
607
+ }
608
+
609
+ get andText() {
610
+ return this.hasAttribute('and-text');
611
+ }
612
+
613
+ set andText(val) {
614
+ if (val) {
615
+ this.setAttribute('and-text', '');
616
+ } else {
617
+ this.removeAttribute('and-text');
618
+ }
619
+ }
620
+
621
+ render() {
622
+ this.style.setProperty('--enedh-max', this.max);
623
+ this.style.setProperty('--enedh-gutters', this.gutters);
624
+
625
+ const intrinsicStr = this.intrinsic ? '-intrinsic' : '';
626
+ const textStr = this.andText ? '-andText' : '';
627
+ const id = `Enedh-${this.max}-${this.gutters}${intrinsicStr}${textStr}`.replace(/[^\w-]/g, '');
628
+
629
+ this.dataset.i = id;
630
+
631
+ if (!document.getElementById(id)) {
632
+ const styleEl = document.createElement('style');
633
+ styleEl.id = id;
634
+
635
+ const selector = `[data-i="${id}"]`;
636
+ let css = `${selector} { max-inline-size: ${this.max}; padding-inline: ${this.gutters}; }`;
637
+
638
+ if (this.intrinsic) {
639
+ css += `${selector} { display: flex; flex-direction: column; align-items: center; }`;
640
+ }
641
+
642
+ if (this.andText) {
643
+ css += `${selector} { text-align: center; }`;
644
+ }
645
+
646
+ styleEl.textContent = css;
647
+ document.head.appendChild(styleEl);
648
+ }
649
+ }
650
+ }
651
+
652
+ if ('customElements' in window) {
653
+ customElements.define('i-enedh', EnedhLayout);
654
+ }
655
+
656
+
657
+
658
+
659
+ // tiniath
660
+ /**
661
+ * Cluster Layout Custom Element
662
+ *
663
+ * Groups elements that differ in length and wrap naturally.
664
+ *
665
+ * @property {string} space - Gap between items (default: var(--s1))
666
+ * @property {string} justify - justify-content value (default: flex-start)
667
+ * @property {string} align - align-items value (default: center)
668
+ *
669
+ * @example
670
+ * <i-tiniath space="var(--s0)" justify="space-between">
671
+ * <button>Save</button>
672
+ * <button>Cancel</button>
673
+ * </i-tiniath>
674
+ */
675
+
676
+ class TiniathLayout extends HTMLElement {
677
+ static get observedAttributes() {
678
+ return ['space', 'justify', 'align'];
679
+ }
680
+
681
+ constructor() {
682
+ super();
683
+ this.render = this.render.bind(this);
684
+ }
685
+
686
+ connectedCallback() {
687
+ this.render();
688
+ }
689
+
690
+ attributeChangedCallback() {
691
+ this.render();
692
+ }
693
+
694
+ get space() {
695
+ return this.getAttribute('space') || 'var(--s1)';
696
+ }
697
+
698
+ set space(val) {
699
+ this.setAttribute('space', val);
700
+ }
701
+
702
+ get justify() {
703
+ return this.getAttribute('justify') || 'flex-start';
704
+ }
705
+
706
+ set justify(val) {
707
+ this.setAttribute('justify', val);
708
+ }
709
+
710
+ get align() {
711
+ return this.getAttribute('align') || 'center';
712
+ }
713
+
714
+ set align(val) {
715
+ this.setAttribute('align', val);
716
+ }
717
+
718
+ render() {
719
+ this.style.setProperty('--tiniath-space', this.space);
720
+ this.style.setProperty('--tiniath-justify', this.justify);
721
+ this.style.setProperty('--tiniath-align', this.align);
722
+
723
+ const id = `Tiniath-${this.space}-${this.justify}-${this.align}`.replace(/[^\w-]/g, '');
724
+ this.dataset.i = id;
725
+
726
+ if (!document.getElementById(id)) {
727
+ const styleEl = document.createElement('style');
728
+ styleEl.id = id;
729
+
730
+ const selector = `[data-i="${id}"]`;
731
+ styleEl.textContent = `
732
+ ${selector} {
733
+ gap: ${this.space};
734
+ justify-content: ${this.justify};
735
+ align-items: ${this.align};
736
+ }
737
+ `;
738
+ document.head.appendChild(styleEl);
739
+ }
740
+ }
741
+ }
742
+
743
+ if ('customElements' in window) {
744
+ customElements.define('i-tiniath', TiniathLayout);
745
+ }
746
+
747
+
748
+
749
+
750
+ // glan-veleg
751
+ /**
752
+ * GlanVeleg Layout Custom Element
753
+ *
754
+ * A quantum layout with one fixed-width element and one fluid element.
755
+ *
756
+ * @property {string} side - Which side is the sidebar: "left" or "right" (default: left)
757
+ * @property {string} sideWidth - Width of sidebar when horizontal (default: 20rem)
758
+ * @property {string} contentMin - Min width of content before wrapping (default: 50%)
759
+ * @property {string} space - Gap between elements (default: var(--s1))
760
+ * @property {boolean} noStretch - Disable equal height (default: false)
761
+ *
762
+ * @example
763
+ * <i-glan-veleg side-width="15rem" content-min="60%">
764
+ * <nav>GlanVeleg content</nav>
765
+ * <main>Main content</main>
766
+ * </i-glan-veleg>
767
+ */
768
+
769
+ class GlanVelegLayout extends HTMLElement {
770
+ static get observedAttributes() {
771
+ return ['side', 'side-width', 'content-min', 'space', 'no-stretch'];
772
+ }
773
+
774
+ constructor() {
775
+ super();
776
+ this.render = this.render.bind(this);
777
+ }
778
+
779
+ connectedCallback() {
780
+ this.render();
781
+ }
782
+
783
+ attributeChangedCallback() {
784
+ this.render();
785
+ }
786
+
787
+ get side() {
788
+ return this.getAttribute('side') || 'left';
789
+ }
790
+
791
+ set side(val) {
792
+ this.setAttribute('side', val);
793
+ }
794
+
795
+ get sideWidth() {
796
+ return this.getAttribute('side-width') || '20rem';
797
+ }
798
+
799
+ set sideWidth(val) {
800
+ this.setAttribute('side-width', val);
801
+ }
802
+
803
+ get contentMin() {
804
+ return this.getAttribute('content-min') || '50%';
805
+ }
806
+
807
+ set contentMin(val) {
808
+ this.setAttribute('content-min', val);
809
+ }
810
+
811
+ get space() {
812
+ return this.getAttribute('space') || 'var(--s1)';
813
+ }
814
+
815
+ set space(val) {
816
+ this.setAttribute('space', val);
817
+ }
818
+
819
+ get noStretch() {
820
+ return this.hasAttribute('no-stretch');
821
+ }
822
+
823
+ set noStretch(val) {
824
+ if (val) {
825
+ this.setAttribute('no-stretch', '');
826
+ } else {
827
+ this.removeAttribute('no-stretch');
828
+ }
829
+ }
830
+
831
+ render() {
832
+ this.style.setProperty('--glan-veleg-width', this.sideWidth);
833
+ this.style.setProperty('--glan-veleg-content-min', this.contentMin);
834
+ this.style.setProperty('--glan-veleg-space', this.space);
835
+
836
+ const sideStr = this.side === 'right' ? '-right' : '-left';
837
+ const stretchStr = this.noStretch ? '-noStretch' : '';
838
+ const id = `GlanVeleg-${this.sideWidth}-${this.contentMin}-${this.space}${sideStr}${stretchStr}`.replace(/[^\w-]/g, '');
839
+
840
+ this.dataset.i = id;
841
+
842
+ if (!document.getElementById(id)) {
843
+ const styleEl = document.createElement('style');
844
+ styleEl.id = id;
845
+
846
+ const selector = `[data-i="${id}"]`;
847
+ let css = `${selector} { gap: ${this.space}; }`;
848
+
849
+ if (this.side === 'right') {
850
+ css += `
851
+ ${selector} > :first-child {
852
+ flex-basis: 0;
853
+ flex-grow: 999;
854
+ min-inline-size: ${this.contentMin};
855
+ }
856
+ ${selector} > :last-child {
857
+ flex-basis: ${this.sideWidth};
858
+ flex-grow: 1;
859
+ }
860
+ `;
861
+ } else {
862
+ css += `
863
+ ${selector} > :first-child {
864
+ flex-basis: ${this.sideWidth};
865
+ }
866
+ ${selector} > :last-child {
867
+ flex-basis: 0;
868
+ flex-grow: 999;
869
+ min-inline-size: ${this.contentMin};
870
+ }
871
+ `;
872
+ }
873
+
874
+ if (this.noStretch) {
875
+ css += `${selector} { align-items: flex-start; }`;
876
+ }
877
+
878
+ styleEl.textContent = css;
879
+ document.head.appendChild(styleEl);
880
+ }
881
+ }
882
+ }
883
+
884
+ if ('customElements' in window) {
885
+ customElements.define('i-glan-veleg', GlanVelegLayout);
886
+ }
887
+
888
+
889
+
890
+
891
+ // gwistindor
892
+ /**
893
+ * Switcher Layout Custom Element
894
+ *
895
+ * Switches between horizontal and vertical at a container threshold.
896
+ * Uses the "Holy Albatross" technique - no intermediary states.
897
+ *
898
+ * @property {string} threshold - Container width to switch at (default: var(--measure))
899
+ * @property {string} space - Gap between elements (default: var(--s1))
900
+ * @property {number} limit - Max items for horizontal layout (default: 4)
901
+ *
902
+ * @example
903
+ * <i-gwistindor threshold="30rem" limit="3">
904
+ * <div>One</div>
905
+ * <div>Two</div>
906
+ * <div>Three</div>
907
+ * </i-gwistindor>
908
+ */
909
+
910
+ class GwistindorLayout extends HTMLElement {
911
+ static get observedAttributes() {
912
+ return ['threshold', 'space', 'limit'];
913
+ }
914
+
915
+ constructor() {
916
+ super();
917
+ this.render = this.render.bind(this);
918
+ }
919
+
920
+ connectedCallback() {
921
+ this.render();
922
+ }
923
+
924
+ attributeChangedCallback() {
925
+ this.render();
926
+ }
927
+
928
+ get threshold() {
929
+ return this.getAttribute('threshold') || 'var(--measure)';
930
+ }
931
+
932
+ set threshold(val) {
933
+ this.setAttribute('threshold', val);
934
+ }
935
+
936
+ get space() {
937
+ return this.getAttribute('space') || 'var(--s1)';
938
+ }
939
+
940
+ set space(val) {
941
+ this.setAttribute('space', val);
942
+ }
943
+
944
+ get limit() {
945
+ return parseInt(this.getAttribute('limit'), 10) || 4;
946
+ }
947
+
948
+ set limit(val) {
949
+ this.setAttribute('limit', val);
950
+ }
951
+
952
+ render() {
953
+ this.style.setProperty('--gwistindor-threshold', this.threshold);
954
+ this.style.setProperty('--gwistindor-space', this.space);
955
+
956
+ const id = `Gwistindor-${this.threshold}-${this.space}-${this.limit}`.replace(/[^\w-]/g, '');
957
+ this.dataset.i = id;
958
+
959
+ if (!document.getElementById(id)) {
960
+ const styleEl = document.createElement('style');
961
+ styleEl.id = id;
962
+
963
+ const selector = `[data-i="${id}"]`;
964
+ const limitPlusOne = this.limit + 1;
965
+
966
+ styleEl.textContent = `
967
+ ${selector} {
968
+ gap: ${this.space};
969
+ }
970
+ ${selector} > * {
971
+ flex-basis: calc((${this.threshold} - 100%) * 999);
972
+ }
973
+ ${selector} > :nth-last-child(n+${limitPlusOne}),
974
+ ${selector} > :nth-last-child(n+${limitPlusOne}) ~ * {
975
+ flex-basis: 100%;
976
+ }
977
+ `;
978
+ document.head.appendChild(styleEl);
979
+ }
980
+ }
981
+ }
982
+
983
+ if ('customElements' in window) {
984
+ customElements.define('i-gwistindor', GwistindorLayout);
985
+ }
986
+
987
+
988
+
989
+
990
+ // esgal
991
+ /**
992
+ * Cover Layout Custom Element
993
+ *
994
+ * Vertically centers a principal element with optional header/footer.
995
+ *
996
+ * @property {string} centered - Selector for the centered element (default: h1)
997
+ * @property {string} space - Minimum space around elements (default: var(--s1))
998
+ * @property {string} minHeight - Minimum height of cover (default: 100vh)
999
+ * @property {boolean} noPad - Remove padding from container
1000
+ *
1001
+ * @example
1002
+ * <i-esgal centered="h2" min-height="80vh">
1003
+ * <header>Logo</header>
1004
+ * <h2>Main Title</h2>
1005
+ * <footer>Scroll down</footer>
1006
+ * </i-esgal>
1007
+ */
1008
+
1009
+ class EsgalLayout extends HTMLElement {
1010
+ static get observedAttributes() {
1011
+ return ['centered', 'space', 'min-height', 'no-pad'];
1012
+ }
1013
+
1014
+ constructor() {
1015
+ super();
1016
+ this.render = this.render.bind(this);
1017
+ }
1018
+
1019
+ connectedCallback() {
1020
+ this.render();
1021
+ }
1022
+
1023
+ attributeChangedCallback() {
1024
+ this.render();
1025
+ }
1026
+
1027
+ get centered() {
1028
+ return this.getAttribute('centered') || 'h1';
1029
+ }
1030
+
1031
+ set centered(val) {
1032
+ this.setAttribute('centered', val);
1033
+ }
1034
+
1035
+ get space() {
1036
+ return this.getAttribute('space') || 'var(--s1)';
1037
+ }
1038
+
1039
+ set space(val) {
1040
+ this.setAttribute('space', val);
1041
+ }
1042
+
1043
+ get minHeight() {
1044
+ return this.getAttribute('min-height') || '100vh';
1045
+ }
1046
+
1047
+ set minHeight(val) {
1048
+ this.setAttribute('min-height', val);
1049
+ }
1050
+
1051
+ get noPad() {
1052
+ return this.hasAttribute('no-pad');
1053
+ }
1054
+
1055
+ set noPad(val) {
1056
+ if (val) {
1057
+ this.setAttribute('no-pad', '');
1058
+ } else {
1059
+ this.removeAttribute('no-pad');
1060
+ }
1061
+ }
1062
+
1063
+ render() {
1064
+ this.style.setProperty('--esgal-min-height', this.minHeight);
1065
+ this.style.setProperty('--esgal-space', this.space);
1066
+
1067
+ const noPadStr = this.noPad ? '-noPad' : '';
1068
+ const id = `Esgal-${this.centered}-${this.minHeight}-${this.space}${noPadStr}`.replace(/[^\w-]/g, '');
1069
+ this.dataset.i = id;
1070
+
1071
+ if (!document.getElementById(id)) {
1072
+ const styleEl = document.createElement('style');
1073
+ styleEl.id = id;
1074
+
1075
+ const selector = `[data-i="${id}"]`;
1076
+ const padding = this.noPad ? '0' : this.space;
1077
+
1078
+ styleEl.textContent = `
1079
+ ${selector} {
1080
+ min-block-size: ${this.minHeight};
1081
+ padding: ${padding};
1082
+ }
1083
+ ${selector} > * {
1084
+ margin-block: ${this.space};
1085
+ }
1086
+ ${selector} > :first-child:not(${this.centered}) {
1087
+ margin-block-start: 0;
1088
+ }
1089
+ ${selector} > :last-child:not(${this.centered}) {
1090
+ margin-block-end: 0;
1091
+ }
1092
+ ${selector} > ${this.centered} {
1093
+ margin-block: auto;
1094
+ }
1095
+ `;
1096
+ document.head.appendChild(styleEl);
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ if ('customElements' in window) {
1102
+ customElements.define('i-esgal', EsgalLayout);
1103
+ }
1104
+
1105
+
1106
+
1107
+
1108
+ // vircantie
1109
+ /**
1110
+ * Vircantie Layout Custom Element
1111
+ *
1112
+ * Auto-flowing responsive grid with minimum column width.
1113
+ *
1114
+ * @property {string} min - Minimum column width (default: 250px)
1115
+ * @property {string} space - Gap between grid cells (default: var(--s1))
1116
+ *
1117
+ * @example
1118
+ * <i-vircantie min="300px" space="var(--s2)">
1119
+ * <div>Card 1</div>
1120
+ * <div>Card 2</div>
1121
+ * <div>Card 3</div>
1122
+ * </i-vircantie>
1123
+ */
1124
+
1125
+ class VircantieLayout extends HTMLElement {
1126
+ static get observedAttributes() {
1127
+ return ['min', 'space'];
1128
+ }
1129
+
1130
+ constructor() {
1131
+ super();
1132
+ this.render = this.render.bind(this);
1133
+ }
1134
+
1135
+ connectedCallback() {
1136
+ this.render();
1137
+ }
1138
+
1139
+ attributeChangedCallback() {
1140
+ this.render();
1141
+ }
1142
+
1143
+ get min() {
1144
+ return this.getAttribute('min') || '250px';
1145
+ }
1146
+
1147
+ set min(val) {
1148
+ this.setAttribute('min', val);
1149
+ }
1150
+
1151
+ get space() {
1152
+ return this.getAttribute('space') || 'var(--s1)';
1153
+ }
1154
+
1155
+ set space(val) {
1156
+ this.setAttribute('space', val);
1157
+ }
1158
+
1159
+ render() {
1160
+ this.style.setProperty('--vircantie-min', this.min);
1161
+ this.style.setProperty('--vircantie-space', this.space);
1162
+
1163
+ const id = `Vircantie-${this.min}-${this.space}`.replace(/[^\w-]/g, '');
1164
+ this.dataset.i = id;
1165
+
1166
+ if (!document.getElementById(id)) {
1167
+ const styleEl = document.createElement('style');
1168
+ styleEl.id = id;
1169
+
1170
+ const selector = `[data-i="${id}"]`;
1171
+
1172
+ styleEl.textContent = `
1173
+ ${selector} {
1174
+ gap: ${this.space};
1175
+ }
1176
+ @supports (width: min(${this.min}, 100%)) {
1177
+ ${selector} {
1178
+ vircantie-template-columns: repeat(auto-fit, minmax(min(${this.min}, 100%), 1fr));
1179
+ }
1180
+ }
1181
+ `;
1182
+ document.head.appendChild(styleEl);
1183
+ }
1184
+ }
1185
+ }
1186
+
1187
+ if ('customElements' in window) {
1188
+ customElements.define('i-vircantie', VircantieLayout);
1189
+ }
1190
+
1191
+
1192
+
1193
+
1194
+ // gant-thala
1195
+ /**
1196
+ * Frame Layout Custom Element
1197
+ *
1198
+ * Constrains content to a specific aspect ratio.
1199
+ *
1200
+ * @property {string} ratio - GantThala ratio as "n:d" (default: 16:9)
1201
+ *
1202
+ * @example
1203
+ * <i-gant-thala ratio="4:3">
1204
+ * <img src="photo.jpg" alt="A photo">
1205
+ * </i-gant-thala>
1206
+ */
1207
+
1208
+ class GantThalaLayout extends HTMLElement {
1209
+ static get observedAttributes() {
1210
+ return ['ratio'];
1211
+ }
1212
+
1213
+ constructor() {
1214
+ super();
1215
+ this.render = this.render.bind(this);
1216
+ }
1217
+
1218
+ connectedCallback() {
1219
+ this.render();
1220
+ }
1221
+
1222
+ attributeChangedCallback() {
1223
+ this.render();
1224
+ }
1225
+
1226
+ get ratio() {
1227
+ return this.getAttribute('ratio') || '16:9';
1228
+ }
1229
+
1230
+ set ratio(val) {
1231
+ this.setAttribute('ratio', val);
1232
+ }
1233
+
1234
+ render() {
1235
+ const [n, d] = this.ratio.split(':').map(v => v.trim());
1236
+
1237
+ this.style.setProperty('--gant-thala-n', n);
1238
+ this.style.setProperty('--gant-thala-d', d);
1239
+
1240
+ const id = `GantThala-${n}-${d}`.replace(/[^\w-]/g, '');
1241
+ this.dataset.i = id;
1242
+
1243
+ if (!document.getElementById(id)) {
1244
+ const styleEl = document.createElement('style');
1245
+ styleEl.id = id;
1246
+
1247
+ const selector = `[data-i="${id}"]`;
1248
+
1249
+ styleEl.textContent = `
1250
+ ${selector} {
1251
+ gant-thala-ratio: ${n} / ${d};
1252
+ }
1253
+ `;
1254
+ document.head.appendChild(styleEl);
1255
+ }
1256
+ }
1257
+ }
1258
+
1259
+ if ('customElements' in window) {
1260
+ customElements.define('i-gant-thala', GantThalaLayout);
1261
+ }
1262
+
1263
+
1264
+
1265
+
1266
+ // glan-tholl
1267
+ /**
1268
+ * Reel Layout Custom Element
1269
+ *
1270
+ * Horizontal scrolling container with native browser scrolling.
1271
+ * Uses ResizeObserver and MutationObserver for overflow detection.
1272
+ *
1273
+ * @property {string} itemWidth - Width of each item (default: auto)
1274
+ * @property {string} space - Gap between items (default: var(--s1))
1275
+ * @property {string} height - Height of the reel (default: auto)
1276
+ * @property {boolean} noBar - Hide the scrollbar
1277
+ *
1278
+ * @example
1279
+ * <i-glan-tholl item-width="300px" space="var(--s2)">
1280
+ * <div>Card 1</div>
1281
+ * <div>Card 2</div>
1282
+ * <div>Card 3</div>
1283
+ * </i-glan-tholl>
1284
+ */
1285
+
1286
+ class GlanThollLayout extends HTMLElement {
1287
+ static get observedAttributes() {
1288
+ return ['item-width', 'space', 'height', 'no-bar'];
1289
+ }
1290
+
1291
+ constructor() {
1292
+ super();
1293
+ this.render = this.render.bind(this);
1294
+ this.toggleOverflowClass = this.toggleOverflowClass.bind(this);
1295
+ this.resizeObserver = null;
1296
+ this.mutationObserver = null;
1297
+ }
1298
+
1299
+ connectedCallback() {
1300
+ this.render();
1301
+ this.setupObservers();
1302
+ }
1303
+
1304
+ disconnectedCallback() {
1305
+ if (this.resizeObserver) {
1306
+ this.resizeObserver.disconnect();
1307
+ }
1308
+ if (this.mutationObserver) {
1309
+ this.mutationObserver.disconnect();
1310
+ }
1311
+ }
1312
+
1313
+ attributeChangedCallback() {
1314
+ this.render();
1315
+ }
1316
+
1317
+ get itemWidth() {
1318
+ return this.getAttribute('item-width') || 'auto';
1319
+ }
1320
+
1321
+ set itemWidth(val) {
1322
+ this.setAttribute('item-width', val);
1323
+ }
1324
+
1325
+ get space() {
1326
+ return this.getAttribute('space') || 'var(--s1)';
1327
+ }
1328
+
1329
+ set space(val) {
1330
+ this.setAttribute('space', val);
1331
+ }
1332
+
1333
+ get height() {
1334
+ return this.getAttribute('height') || 'auto';
1335
+ }
1336
+
1337
+ set height(val) {
1338
+ this.setAttribute('height', val);
1339
+ }
1340
+
1341
+ get noBar() {
1342
+ return this.hasAttribute('no-bar');
1343
+ }
1344
+
1345
+ set noBar(val) {
1346
+ if (val) {
1347
+ this.setAttribute('no-bar', '');
1348
+ } else {
1349
+ this.removeAttribute('no-bar');
1350
+ }
1351
+ }
1352
+
1353
+ toggleOverflowClass() {
1354
+ this.classList.toggle('overflowing', this.scrollWidth > this.clientWidth);
1355
+ }
1356
+
1357
+ setupObservers() {
1358
+ // ResizeObserver for container size changes
1359
+ if ('ResizeObserver' in window) {
1360
+ this.resizeObserver = new ResizeObserver(entries => {
1361
+ this.toggleOverflowClass();
1362
+ });
1363
+ this.resizeObserver.observe(this);
1364
+ }
1365
+
1366
+ // MutationObserver for child changes
1367
+ if ('MutationObserver' in window) {
1368
+ this.mutationObserver = new MutationObserver(entries => {
1369
+ this.toggleOverflowClass();
1370
+ });
1371
+ this.mutationObserver.observe(this, { childList: true });
1372
+ }
1373
+
1374
+ // Initial check
1375
+ this.toggleOverflowClass();
1376
+ }
1377
+
1378
+ render() {
1379
+ this.style.setProperty('--glan-tholl-item-width', this.itemWidth);
1380
+ this.style.setProperty('--glan-tholl-space', this.space);
1381
+ this.style.setProperty('--glan-tholl-height', this.height);
1382
+
1383
+ const noBarStr = this.noBar ? '-noBar' : '';
1384
+ const id = `GlanTholl-${this.itemWidth}-${this.space}-${this.height}${noBarStr}`.replace(/[^\w-]/g, '');
1385
+ this.dataset.i = id;
1386
+
1387
+ if (!document.getElementById(id)) {
1388
+ const styleEl = document.createElement('style');
1389
+ styleEl.id = id;
1390
+
1391
+ const selector = `[data-i="${id}"]`;
1392
+
1393
+ styleEl.textContent = `
1394
+ ${selector} {
1395
+ block-size: ${this.height};
1396
+ }
1397
+ ${selector} > * {
1398
+ flex: 0 0 ${this.itemWidth};
1399
+ }
1400
+ ${selector} > * + * {
1401
+ margin-inline-start: ${this.space};
1402
+ }
1403
+ `;
1404
+ document.head.appendChild(styleEl);
1405
+ }
1406
+ }
1407
+ }
1408
+
1409
+ if ('customElements' in window) {
1410
+ customElements.define('i-glan-tholl', GlanThollLayout);
1411
+ }
1412
+
1413
+
1414
+
1415
+
1416
+ // fano
1417
+ /**
1418
+ * Imposter Layout Custom Element
1419
+ *
1420
+ * Positions an element centered over a positioning container.
1421
+ *
1422
+ * @property {boolean} fixed - Use fixed positioning (viewport-relative)
1423
+ * @property {boolean} contain - Prevent overflow outside container
1424
+ * @property {string} margin - Minimum gap from container edges (when contained)
1425
+ *
1426
+ * @example
1427
+ * <div style="position: relative;">
1428
+ * <p>Background content</p>
1429
+ * <i-fano contain margin="var(--s1)">
1430
+ * <dialog open>Modal content</dialog>
1431
+ * </i-fano>
1432
+ * </div>
1433
+ */
1434
+
1435
+ class FanoLayout extends HTMLElement {
1436
+ static get observedAttributes() {
1437
+ return ['fixed', 'contain', 'margin'];
1438
+ }
1439
+
1440
+ constructor() {
1441
+ super();
1442
+ this.render = this.render.bind(this);
1443
+ }
1444
+
1445
+ connectedCallback() {
1446
+ this.render();
1447
+ }
1448
+
1449
+ attributeChangedCallback() {
1450
+ this.render();
1451
+ }
1452
+
1453
+ get fixed() {
1454
+ return this.hasAttribute('fixed');
1455
+ }
1456
+
1457
+ set fixed(val) {
1458
+ if (val) {
1459
+ this.setAttribute('fixed', '');
1460
+ } else {
1461
+ this.removeAttribute('fixed');
1462
+ }
1463
+ }
1464
+
1465
+ get contain() {
1466
+ return this.hasAttribute('contain');
1467
+ }
1468
+
1469
+ set contain(val) {
1470
+ if (val) {
1471
+ this.setAttribute('contain', '');
1472
+ } else {
1473
+ this.removeAttribute('contain');
1474
+ }
1475
+ }
1476
+
1477
+ get margin() {
1478
+ return this.getAttribute('margin') || '0px';
1479
+ }
1480
+
1481
+ set margin(val) {
1482
+ this.setAttribute('margin', val);
1483
+ }
1484
+
1485
+ render() {
1486
+ this.style.setProperty('--fano-margin', this.margin);
1487
+
1488
+ const fixedStr = this.fixed ? '-fixed' : '';
1489
+ const containStr = this.contain ? '-contain' : '';
1490
+ const id = `Fano-${this.margin}${fixedStr}${containStr}`.replace(/[^\w-]/g, '');
1491
+ this.dataset.i = id;
1492
+
1493
+ if (!document.getElementById(id)) {
1494
+ const styleEl = document.createElement('style');
1495
+ styleEl.id = id;
1496
+
1497
+ const selector = `[data-i="${id}"]`;
1498
+ let css = '';
1499
+
1500
+ if (this.fixed) {
1501
+ css += `${selector} { position: fixed; }`;
1502
+ }
1503
+
1504
+ if (this.contain) {
1505
+ css += `
1506
+ ${selector} {
1507
+ overflow: auto;
1508
+ max-inline-size: calc(100% - (${this.margin} * 2));
1509
+ max-block-size: calc(100% - (${this.margin} * 2));
1510
+ }
1511
+ `;
1512
+ }
1513
+
1514
+ styleEl.textContent = css;
1515
+ document.head.appendChild(styleEl);
1516
+ }
1517
+ }
1518
+ }
1519
+
1520
+ if ('customElements' in window) {
1521
+ customElements.define('i-fano', FanoLayout);
1522
+ }
1523
+
1524
+
1525
+
1526
+
1527
+ // thann
1528
+ /**
1529
+ * Thann Layout Custom Element
1530
+ *
1531
+ * Aligns an SVG icon with accompanying text.
1532
+ *
1533
+ * @property {string} space - Gap between icon and text (default: natural word space)
1534
+ * @property {string} label - Accessible label for standalone icons
1535
+ *
1536
+ * @example
1537
+ * <i-thann space="0.5em">
1538
+ * <svg>...</svg>
1539
+ * Close
1540
+ * </i-thann>
1541
+ */
1542
+
1543
+ class ThannLayout extends HTMLElement {
1544
+ static get observedAttributes() {
1545
+ return ['space', 'label'];
1546
+ }
1547
+
1548
+ constructor() {
1549
+ super();
1550
+ this.render = this.render.bind(this);
1551
+ }
1552
+
1553
+ connectedCallback() {
1554
+ this.render();
1555
+ }
1556
+
1557
+ attributeChangedCallback() {
1558
+ this.render();
1559
+ }
1560
+
1561
+ get space() {
1562
+ return this.getAttribute('space');
1563
+ }
1564
+
1565
+ set space(val) {
1566
+ if (val) {
1567
+ this.setAttribute('space', val);
1568
+ } else {
1569
+ this.removeAttribute('space');
1570
+ }
1571
+ }
1572
+
1573
+ get label() {
1574
+ return this.getAttribute('label');
1575
+ }
1576
+
1577
+ set label(val) {
1578
+ if (val) {
1579
+ this.setAttribute('label', val);
1580
+ } else {
1581
+ this.removeAttribute('label');
1582
+ }
1583
+ }
1584
+
1585
+ render() {
1586
+ // Handle accessibility for standalone icons
1587
+ if (this.label) {
1588
+ this.setAttribute('role', 'img');
1589
+ this.setAttribute('aria-label', this.label);
1590
+ } else {
1591
+ this.removeAttribute('role');
1592
+ this.removeAttribute('aria-label');
1593
+ }
1594
+
1595
+ // Only set custom property if space is specified
1596
+ if (this.space) {
1597
+ this.style.setProperty('--thann-space', this.space);
1598
+
1599
+ const id = `Thann-${this.space}`.replace(/[^\w-]/g, '');
1600
+ this.dataset.i = id;
1601
+
1602
+ if (!document.getElementById(id)) {
1603
+ const styleEl = document.createElement('style');
1604
+ styleEl.id = id;
1605
+
1606
+ const selector = `[data-i="${id}"]`;
1607
+
1608
+ styleEl.textContent = `
1609
+ ${selector} {
1610
+ gap: ${this.space};
1611
+ }
1612
+ `;
1613
+ document.head.appendChild(styleEl);
1614
+ }
1615
+ }
1616
+ }
1617
+ }
1618
+
1619
+ if ('customElements' in window) {
1620
+ customElements.define('i-thann', ThannLayout);
1621
+ }
1622
+
1623
+
1624
+
1625
+
1626
+ // adleithian
1627
+ /**
1628
+ * Adleithian Layout Custom Element
1629
+ *
1630
+ * Establishes a container query context.
1631
+ *
1632
+ * @property {string} name - Adleithian name for targeted queries (optional)
1633
+ *
1634
+ * @example
1635
+ * <i-adleithian name="card">
1636
+ * <div class="card">...</div>
1637
+ * </i-adleithian>
1638
+ *
1639
+ * CSS:
1640
+ * @container card (width < 300px) {
1641
+ * .card { flex-direction: column; }
1642
+ * }
1643
+ */
1644
+
1645
+ class AdleithianLayout extends HTMLElement {
1646
+ static get observedAttributes() {
1647
+ return ['name'];
1648
+ }
1649
+
1650
+ constructor() {
1651
+ super();
1652
+ this.render = this.render.bind(this);
1653
+ }
1654
+
1655
+ connectedCallback() {
1656
+ this.render();
1657
+ }
1658
+
1659
+ attributeChangedCallback() {
1660
+ this.render();
1661
+ }
1662
+
1663
+ get name() {
1664
+ return this.getAttribute('name');
1665
+ }
1666
+
1667
+ set name(val) {
1668
+ if (val) {
1669
+ this.setAttribute('name', val);
1670
+ } else {
1671
+ this.removeAttribute('name');
1672
+ }
1673
+ }
1674
+
1675
+ render() {
1676
+ // Set adleithian-name via style if provided
1677
+ if (this.name) {
1678
+ this.style.containerName = this.name;
1679
+ } else {
1680
+ this.style.containerName = '';
1681
+ }
1682
+ }
1683
+ }
1684
+
1685
+ if ('customElements' in window) {
1686
+ customElements.define('i-adleithian', AdleithianLayout);
1687
+ }
1688
+
1689
+
1690
+
1691
+
1692
+ // him
1693
+ /**
1694
+ * Him Layout Custom Element (NEW)
1695
+ *
1696
+ * Creates sticky positioning with configurable offset.
1697
+ * Optionally detects "stuck" state via IntersectionObserver.
1698
+ *
1699
+ * @property {string} to - Direction to stick: top, bottom, left, right (default: top)
1700
+ * @property {string} offset - Distance from edge when stuck (default: 0)
1701
+ * @property {boolean} sentinel - Enable stuck state detection
1702
+ *
1703
+ * @example
1704
+ * <i-him offset="var(--s1)" sentinel>
1705
+ * <header>This header sticks</header>
1706
+ * </i-him>
1707
+ */
1708
+
1709
+ class HimLayout extends HTMLElement {
1710
+ static get observedAttributes() {
1711
+ return ['to', 'offset', 'sentinel'];
1712
+ }
1713
+
1714
+ constructor() {
1715
+ super();
1716
+ this.render = this.render.bind(this);
1717
+ this.observer = null;
1718
+ this.sentinelEl = null;
1719
+ }
1720
+
1721
+ connectedCallback() {
1722
+ this.render();
1723
+ }
1724
+
1725
+ disconnectedCallback() {
1726
+ this.cleanupSentinel();
1727
+ }
1728
+
1729
+ attributeChangedCallback() {
1730
+ this.render();
1731
+ }
1732
+
1733
+ get to() {
1734
+ return this.getAttribute('to') || 'top';
1735
+ }
1736
+
1737
+ set to(val) {
1738
+ this.setAttribute('to', val);
1739
+ }
1740
+
1741
+ get offset() {
1742
+ return this.getAttribute('offset') || '0';
1743
+ }
1744
+
1745
+ set offset(val) {
1746
+ this.setAttribute('offset', val);
1747
+ }
1748
+
1749
+ get sentinel() {
1750
+ return this.hasAttribute('sentinel');
1751
+ }
1752
+
1753
+ set sentinel(val) {
1754
+ if (val) {
1755
+ this.setAttribute('sentinel', '');
1756
+ } else {
1757
+ this.removeAttribute('sentinel');
1758
+ }
1759
+ }
1760
+
1761
+ cleanupSentinel() {
1762
+ if (this.observer) {
1763
+ this.observer.disconnect();
1764
+ this.observer = null;
1765
+ }
1766
+ if (this.sentinelEl) {
1767
+ this.sentinelEl.remove();
1768
+ this.sentinelEl = null;
1769
+ }
1770
+ }
1771
+
1772
+ setupSentinel() {
1773
+ this.cleanupSentinel();
1774
+
1775
+ if (!this.sentinel || !('IntersectionObserver' in window)) {
1776
+ return;
1777
+ }
1778
+
1779
+ // Create invisible sentinel element
1780
+ this.sentinelEl = document.createElement('div');
1781
+ this.sentinelEl.style.cssText = `
1782
+ position: absolute;
1783
+ height: 1px;
1784
+ width: 100%;
1785
+ pointer-events: none;
1786
+ visibility: hidden;
1787
+ `;
1788
+
1789
+ // Position sentinel based on sticky direction
1790
+ if (this.to === 'bottom') {
1791
+ this.sentinelEl.style.bottom = '0';
1792
+ this.insertAdjacentElement('afterend', this.sentinelEl);
1793
+ } else {
1794
+ this.sentinelEl.style.top = '0';
1795
+ this.insertAdjacentElement('beforebegin', this.sentinelEl);
1796
+ }
1797
+
1798
+ // Observe the sentinel
1799
+ this.observer = new IntersectionObserver(
1800
+ (entries) => {
1801
+ entries.forEach((entry) => {
1802
+ // When sentinel is not visible, we're "stuck"
1803
+ const isStuck = !entry.isIntersecting;
1804
+ this.dataset.stuck = isStuck;
1805
+ this.dispatchEvent(new CustomEvent('stuck-change', {
1806
+ detail: { stuck: isStuck }
1807
+ }));
1808
+ });
1809
+ },
1810
+ { threshold: [0] }
1811
+ );
1812
+
1813
+ this.observer.observe(this.sentinelEl);
1814
+ }
1815
+
1816
+ render() {
1817
+ this.style.setProperty('--him-offset', this.offset);
1818
+
1819
+ const id = `Him-${this.to}-${this.offset}`.replace(/[^\w-]/g, '');
1820
+ this.dataset.i = id;
1821
+
1822
+ if (!document.getElementById(id)) {
1823
+ const styleEl = document.createElement('style');
1824
+ styleEl.id = id;
1825
+
1826
+ const selector = `[data-i="${id}"]`;
1827
+ let css = '';
1828
+
1829
+ switch (this.to) {
1830
+ case 'bottom':
1831
+ css = `${selector} { inset-block-start: auto; inset-block-end: ${this.offset}; }`;
1832
+ break;
1833
+ case 'left':
1834
+ css = `${selector} { inset-block-start: auto; inset-inline-start: ${this.offset}; }`;
1835
+ break;
1836
+ case 'right':
1837
+ css = `${selector} { inset-block-start: auto; inset-inline-end: ${this.offset}; }`;
1838
+ break;
1839
+ default: // top
1840
+ css = `${selector} { inset-block-start: ${this.offset}; }`;
1841
+ }
1842
+
1843
+ styleEl.textContent = css;
1844
+ document.head.appendChild(styleEl);
1845
+ }
1846
+
1847
+ // Setup or cleanup sentinel observation
1848
+ if (this.sentinel) {
1849
+ // Defer to ensure element is in DOM
1850
+ requestAnimationFrame(() => this.setupSentinel());
1851
+ } else {
1852
+ this.cleanupSentinel();
1853
+ }
1854
+ }
1855
+ }
1856
+
1857
+ if ('customElements' in window) {
1858
+ customElements.define('i-him', HimLayout);
1859
+ }
1860
+
1861
+
1862
+
1863
+
1864
+ // miriant
1865
+ /**
1866
+ * Grid-Placed Layout Custom Element (NEW)
1867
+ *
1868
+ * A CSS Grid with explicit control over columns, rows, and item placement.
1869
+ *
1870
+ * @property {number} columns - Number of grid columns (default: 12)
1871
+ * @property {string} space - Gap between grid cells (default: var(--s1))
1872
+ * @property {string} rowHeight - Height of auto-generated rows (default: minmax(0, auto))
1873
+ * @property {boolean} dense - Enable dense packing to fill holes
1874
+ *
1875
+ * Children can use data attributes for placement:
1876
+ * - data-col-span="N" - Span N columns (1-12 or "full")
1877
+ * - data-row-span="N" - Span N rows (1-6)
1878
+ * - data-col-start="N" - Start at column N
1879
+ * - data-row-start="N" - Start at row N
1880
+ *
1881
+ * @example
1882
+ * <i-miriant columns="4" space="var(--s2)">
1883
+ * <div data-col-span="2" data-row-span="2">Feature</div>
1884
+ * <div>Small</div>
1885
+ * <div>Small</div>
1886
+ * <div data-col-span="full">Full width</div>
1887
+ * </i-miriant>
1888
+ */
1889
+
1890
+ class MiriantLayout extends HTMLElement {
1891
+ static get observedAttributes() {
1892
+ return ['columns', 'space', 'row-height', 'dense'];
1893
+ }
1894
+
1895
+ constructor() {
1896
+ super();
1897
+ this.render = this.render.bind(this);
1898
+ this.mutationObserver = null;
1899
+ }
1900
+
1901
+ connectedCallback() {
1902
+ this.render();
1903
+ this.setupChildObserver();
1904
+ }
1905
+
1906
+ disconnectedCallback() {
1907
+ if (this.mutationObserver) {
1908
+ this.mutationObserver.disconnect();
1909
+ }
1910
+ }
1911
+
1912
+ attributeChangedCallback() {
1913
+ this.render();
1914
+ }
1915
+
1916
+ get columns() {
1917
+ return parseInt(this.getAttribute('columns'), 10) || 12;
1918
+ }
1919
+
1920
+ set columns(val) {
1921
+ this.setAttribute('columns', val);
1922
+ }
1923
+
1924
+ get space() {
1925
+ return this.getAttribute('space') || 'var(--s1)';
1926
+ }
1927
+
1928
+ set space(val) {
1929
+ this.setAttribute('space', val);
1930
+ }
1931
+
1932
+ get rowHeight() {
1933
+ return this.getAttribute('row-height') || 'minmax(0, auto)';
1934
+ }
1935
+
1936
+ set rowHeight(val) {
1937
+ this.setAttribute('row-height', val);
1938
+ }
1939
+
1940
+ get dense() {
1941
+ return this.hasAttribute('dense');
1942
+ }
1943
+
1944
+ set dense(val) {
1945
+ if (val) {
1946
+ this.setAttribute('dense', '');
1947
+ } else {
1948
+ this.removeAttribute('dense');
1949
+ }
1950
+ }
1951
+
1952
+ setupChildObserver() {
1953
+ if (!('MutationObserver' in window)) return;
1954
+
1955
+ this.mutationObserver = new MutationObserver((mutations) => {
1956
+ mutations.forEach((mutation) => {
1957
+ if (mutation.type === 'childList') {
1958
+ this.styleChildren();
1959
+ }
1960
+ });
1961
+ });
1962
+
1963
+ this.mutationObserver.observe(this, { childList: true });
1964
+ this.styleChildren();
1965
+ }
1966
+
1967
+ styleChildren() {
1968
+ // Apply inline styles for explicit placement (data-col-start, data-row-start)
1969
+ Array.from(this.children).forEach((child) => {
1970
+ const colStart = child.dataset.colStart;
1971
+ const rowStart = child.dataset.rowStart;
1972
+
1973
+ if (colStart) {
1974
+ child.style.gridColumnStart = colStart;
1975
+ }
1976
+ if (rowStart) {
1977
+ child.style.gridRowStart = rowStart;
1978
+ }
1979
+ });
1980
+ }
1981
+
1982
+ render() {
1983
+ this.style.setProperty('--miriant-columns', this.columns);
1984
+ this.style.setProperty('--miriant-space', this.space);
1985
+ this.style.setProperty('--miriant-row-height', this.rowHeight);
1986
+
1987
+ const denseStr = this.dense ? '-dense' : '';
1988
+ const id = `Miriant-${this.columns}-${this.space}-${this.rowHeight}${denseStr}`.replace(/[^\w-]/g, '');
1989
+ this.dataset.i = id;
1990
+
1991
+ // Set container-type for container queries
1992
+ this.style.containerType = 'inline-size';
1993
+
1994
+ if (!document.getElementById(id)) {
1995
+ const styleEl = document.createElement('style');
1996
+ styleEl.id = id;
1997
+
1998
+ const selector = `[data-i="${id}"]`;
1999
+
2000
+ let css = `
2001
+ ${selector} {
2002
+ grid-template-columns: repeat(${this.columns}, 1fr);
2003
+ gap: ${this.space};
2004
+ grid-auto-rows: ${this.rowHeight};
2005
+ }
2006
+ `;
2007
+
2008
+ if (this.dense) {
2009
+ css += `${selector} { grid-auto-flow: dense; }`;
2010
+ }
2011
+
2012
+ styleEl.textContent = css;
2013
+ document.head.appendChild(styleEl);
2014
+ }
2015
+ }
2016
+ }
2017
+
2018
+ if ('customElements' in window) {
2019
+ customElements.define('i-miriant', MiriantLayout);
2020
+ }
2021
+
2022
+
2023
+
2024
+
2025
+ // gonath
2026
+ /**
2027
+ * Gonath Layout Custom Element (NEW)
2028
+ *
2029
+ * Pinterest-style layout with efficient column packing.
2030
+ *
2031
+ * Uses CSS columns (widely supported) with progressive enhancement
2032
+ * to CSS Grid masonry where available.
2033
+ *
2034
+ * @property {number} columns - Number of columns (default: 3)
2035
+ * @property {string} space - Gap between items (default: var(--s1))
2036
+ *
2037
+ * NOTE: CSS columns flow top-to-bottom, then left-to-right.
2038
+ * This means visual order differs from DOM order.
2039
+ * For true masonry ordering, JavaScript is required.
2040
+ *
2041
+ * @example
2042
+ * <i-gonath columns="4" space="var(--s2)">
2043
+ * <div>Item 1</div>
2044
+ * <div>Taller Item 2</div>
2045
+ * <div>Item 3</div>
2046
+ * ...
2047
+ * </i-gonath>
2048
+ */
2049
+
2050
+ class GonathLayout extends HTMLElement {
2051
+ static get observedAttributes() {
2052
+ return ['columns', 'space'];
2053
+ }
2054
+
2055
+ constructor() {
2056
+ super();
2057
+ this.render = this.render.bind(this);
2058
+ }
2059
+
2060
+ connectedCallback() {
2061
+ this.render();
2062
+ }
2063
+
2064
+ attributeChangedCallback() {
2065
+ this.render();
2066
+ }
2067
+
2068
+ get columns() {
2069
+ return parseInt(this.getAttribute('columns'), 10) || 3;
2070
+ }
2071
+
2072
+ set columns(val) {
2073
+ this.setAttribute('columns', val);
2074
+ }
2075
+
2076
+ get space() {
2077
+ return this.getAttribute('space') || 'var(--s1)';
2078
+ }
2079
+
2080
+ set space(val) {
2081
+ this.setAttribute('space', val);
2082
+ }
2083
+
2084
+ // Check if CSS Grid masonry is supported
2085
+ static get isGridGonathSupported() {
2086
+ return CSS.supports('grid-template-rows', 'masonry');
2087
+ }
2088
+
2089
+ render() {
2090
+ this.style.setProperty('--gonath-columns', this.columns);
2091
+ this.style.setProperty('--gonath-space', this.space);
2092
+
2093
+ const id = `Gonath-${this.columns}-${this.space}`.replace(/[^\w-]/g, '');
2094
+ this.dataset.i = id;
2095
+
2096
+ if (!document.getElementById(id)) {
2097
+ const styleEl = document.createElement('style');
2098
+ styleEl.id = id;
2099
+
2100
+ const selector = `[data-i="${id}"]`;
2101
+
2102
+ let css = `
2103
+ ${selector} {
2104
+ column-count: ${this.columns};
2105
+ column-gap: ${this.space};
2106
+ }
2107
+ ${selector} > * {
2108
+ margin-block-end: ${this.space};
2109
+ }
2110
+ `;
2111
+
2112
+ // Progressive enhancement for CSS Grid masonry
2113
+ css += `
2114
+ @supports (grid-template-rows: masonry) {
2115
+ ${selector} {
2116
+ display: grid;
2117
+ column-count: unset;
2118
+ grid-template-columns: repeat(${this.columns}, 1fr);
2119
+ grid-template-rows: masonry;
2120
+ gap: ${this.space};
2121
+ }
2122
+ ${selector} > * {
2123
+ margin-block-end: 0;
2124
+ }
2125
+ }
2126
+ `;
2127
+
2128
+ styleEl.textContent = css;
2129
+ document.head.appendChild(styleEl);
2130
+ }
2131
+ }
2132
+ }
2133
+
2134
+ if ('customElements' in window) {
2135
+ customElements.define('i-gonath', GonathLayout);
2136
+ }
2137
+
2138
+
2139
+
2140
+
2141
+
2142
+
2143
+ // Expose to window
2144
+ window.Elvish = {
2145
+ HathLayout,
2146
+ BauLayout,
2147
+ EnedhLayout,
2148
+ TiniathLayout,
2149
+ GlanVelegLayout,
2150
+ GwistindorLayout,
2151
+ EsgalLayout,
2152
+ VircantieLayout,
2153
+ GantThalaLayout,
2154
+ GlanThollLayout,
2155
+ FanoLayout,
2156
+ ThannLayout,
2157
+ AdleithianLayout,
2158
+ HimLayout,
2159
+ MiriantLayout,
2160
+ GonathLayout,
2161
+ transition: transition,
2162
+ transitionTo: transitionTo,
2163
+ transitionTheme: transitionTheme,
2164
+ transitionRatio: transitionRatio,
2165
+ transitionLayout: transitionLayout,
2166
+ supportsViewTransitions: supportsViewTransitions,
2167
+ VERSION: '2.0.0'
2168
+ };
2169
+ })();