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