liqgui 0.1.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 (51) hide show
  1. package/README.md +190 -0
  2. package/dist/components/glass-accordion.d.ts +15 -0
  3. package/dist/components/glass-accordion.js +173 -0
  4. package/dist/components/glass-avatar.d.ts +9 -0
  5. package/dist/components/glass-avatar.js +98 -0
  6. package/dist/components/glass-badge.d.ts +10 -0
  7. package/dist/components/glass-badge.js +151 -0
  8. package/dist/components/glass-button.d.ts +6 -0
  9. package/dist/components/glass-button.js +124 -0
  10. package/dist/components/glass-card.d.ts +8 -0
  11. package/dist/components/glass-card.js +102 -0
  12. package/dist/components/glass-dropdown.d.ts +12 -0
  13. package/dist/components/glass-dropdown.js +182 -0
  14. package/dist/components/glass-input.d.ts +8 -0
  15. package/dist/components/glass-input.js +151 -0
  16. package/dist/components/glass-modal.d.ts +11 -0
  17. package/dist/components/glass-modal.js +128 -0
  18. package/dist/components/glass-navbar.d.ts +6 -0
  19. package/dist/components/glass-navbar.js +84 -0
  20. package/dist/components/glass-progress.d.ts +12 -0
  21. package/dist/components/glass-progress.js +159 -0
  22. package/dist/components/glass-slider.d.ts +17 -0
  23. package/dist/components/glass-slider.js +168 -0
  24. package/dist/components/glass-tabs.d.ts +7 -0
  25. package/dist/components/glass-tabs.js +102 -0
  26. package/dist/components/glass-toast.d.ts +8 -0
  27. package/dist/components/glass-toast.js +128 -0
  28. package/dist/components/glass-toggle.d.ts +9 -0
  29. package/dist/components/glass-toggle.js +112 -0
  30. package/dist/components/glass-tooltip.d.ts +14 -0
  31. package/dist/components/glass-tooltip.js +214 -0
  32. package/dist/core/base-element.d.ts +4 -0
  33. package/dist/core/base-element.js +9 -0
  34. package/dist/core/curves.d.ts +22 -0
  35. package/dist/core/curves.js +32 -0
  36. package/dist/core/focus-trap.d.ts +1 -0
  37. package/dist/core/focus-trap.js +19 -0
  38. package/dist/core/glow.d.ts +3 -0
  39. package/dist/core/glow.js +57 -0
  40. package/dist/core/motion.d.ts +12 -0
  41. package/dist/core/motion.js +54 -0
  42. package/dist/core/spring-engine.d.ts +12 -0
  43. package/dist/core/spring-engine.js +90 -0
  44. package/dist/core/supports.d.ts +1 -0
  45. package/dist/core/supports.js +1 -0
  46. package/dist/core/theme.d.ts +2 -0
  47. package/dist/core/theme.js +1 -0
  48. package/dist/index.d.ts +39 -0
  49. package/dist/index.js +40 -0
  50. package/package.json +47 -0
  51. package/src/styles/tokens.css +140 -0
@@ -0,0 +1,128 @@
1
+ import { BaseElement } from "../core/base-element.js";
2
+ import { trapFocus } from "../core/focus-trap.js";
3
+ import { motion } from "../core/motion.js";
4
+ export class GlassModal extends BaseElement {
5
+ static get observedAttributes() {
6
+ return ["open"];
7
+ }
8
+ connectedCallback() {
9
+ var _a;
10
+ this.mount(`
11
+ <div class="backdrop"></div>
12
+ <div class="panel" role="dialog" aria-modal="true">
13
+ <button class="close-btn" aria-label="Close">
14
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15
+ <path d="M18 6L6 18M6 6l12 12"/>
16
+ </svg>
17
+ </button>
18
+ <slot></slot>
19
+ </div>
20
+ `, `
21
+ :host {
22
+ position: fixed;
23
+ inset: 0;
24
+ display: none;
25
+ place-items: center;
26
+ z-index: 9999;
27
+ }
28
+ :host([open]) {
29
+ display: grid;
30
+ }
31
+ .backdrop {
32
+ position: absolute;
33
+ inset: 0;
34
+ background: rgba(0, 0, 0, 0.6);
35
+ backdrop-filter: blur(4px);
36
+ -webkit-backdrop-filter: blur(4px);
37
+ }
38
+ .panel {
39
+ position: relative;
40
+ background: var(--lg-bg);
41
+ backdrop-filter: blur(var(--lg-blur));
42
+ -webkit-backdrop-filter: blur(var(--lg-blur));
43
+ border-radius: var(--lg-radius);
44
+ border: 1px solid var(--lg-border);
45
+ box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
46
+ padding: 2rem;
47
+ min-width: 320px;
48
+ max-width: 90vw;
49
+ max-height: 85vh;
50
+ overflow: auto;
51
+ }
52
+ .panel::before {
53
+ content: "";
54
+ position: absolute;
55
+ top: 0;
56
+ left: 0;
57
+ right: 0;
58
+ height: 1px;
59
+ background: linear-gradient(90deg,
60
+ transparent,
61
+ rgba(255, 255, 255, 0.4),
62
+ transparent
63
+ );
64
+ }
65
+ .close-btn {
66
+ position: absolute;
67
+ top: 1rem;
68
+ right: 1rem;
69
+ width: 32px;
70
+ height: 32px;
71
+ border-radius: 50%;
72
+ background: rgba(255, 255, 255, 0.1);
73
+ border: 1px solid rgba(255, 255, 255, 0.2);
74
+ color: inherit;
75
+ cursor: pointer;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ transition: all 0.2s ease;
80
+ }
81
+ .close-btn:hover {
82
+ background: rgba(255, 255, 255, 0.2);
83
+ }
84
+ .close-btn:focus-visible {
85
+ outline: 2px solid var(--lg-accent-focus, #5ac8fa);
86
+ outline-offset: 2px;
87
+ }
88
+ `);
89
+ this.panel = this.root.querySelector(".panel");
90
+ this.backdrop = this.root.querySelector(".backdrop");
91
+ // Close on backdrop click
92
+ this.backdrop.addEventListener("click", () => this.close());
93
+ // Close button
94
+ (_a = this.root.querySelector(".close-btn")) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => this.close());
95
+ // Close on Escape
96
+ this.addEventListener("keydown", (e) => {
97
+ if (e.key === "Escape")
98
+ this.close();
99
+ });
100
+ }
101
+ attributeChangedCallback(name, oldValue, newValue) {
102
+ var _a;
103
+ if (name !== "open" || !this.panel || !this.backdrop)
104
+ return;
105
+ if (this.hasAttribute("open")) {
106
+ // Opening animation
107
+ motion.fadeIn(this.backdrop, 200);
108
+ motion.scaleIn(this.panel, 300);
109
+ this.release = trapFocus(this.panel);
110
+ document.body.style.overflow = "hidden";
111
+ }
112
+ else if (oldValue !== null) {
113
+ // Closing animation
114
+ motion.fadeOut(this.backdrop, 150);
115
+ motion.scaleOut(this.panel, 200);
116
+ (_a = this.release) === null || _a === void 0 ? void 0 : _a.call(this);
117
+ document.body.style.overflow = "";
118
+ }
119
+ }
120
+ open() {
121
+ this.setAttribute("open", "");
122
+ }
123
+ close() {
124
+ this.removeAttribute("open");
125
+ this.dispatchEvent(new CustomEvent("close", { bubbles: true }));
126
+ }
127
+ }
128
+ customElements.define("glass-modal", GlassModal);
@@ -0,0 +1,6 @@
1
+ import { BaseElement } from "../core/base-element.js";
2
+ export declare class GlassNavbar extends BaseElement {
3
+ private lastScroll;
4
+ connectedCallback(): void;
5
+ private handleScroll;
6
+ }
@@ -0,0 +1,84 @@
1
+ import { BaseElement } from "../core/base-element.js";
2
+ export class GlassNavbar extends BaseElement {
3
+ constructor() {
4
+ super(...arguments);
5
+ this.lastScroll = 0;
6
+ }
7
+ connectedCallback() {
8
+ this.mount(`
9
+ <nav>
10
+ <div class="brand"><slot name="brand"></slot></div>
11
+ <div class="content"><slot></slot></div>
12
+ <div class="actions"><slot name="actions"></slot></div>
13
+ </nav>
14
+ `, `
15
+ :host {
16
+ display: block;
17
+ position: sticky;
18
+ top: 0;
19
+ z-index: 100;
20
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
21
+ }
22
+ :host([hidden-nav]) {
23
+ transform: translateY(-100%);
24
+ }
25
+ nav {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: space-between;
29
+ gap: 2rem;
30
+ padding: 1rem 2rem;
31
+ background: var(--lg-bg);
32
+ backdrop-filter: blur(var(--lg-blur));
33
+ -webkit-backdrop-filter: blur(var(--lg-blur));
34
+ border-bottom: 1px solid var(--lg-border);
35
+ transition: all 0.3s ease;
36
+ }
37
+ :host([scrolled]) nav {
38
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
39
+ }
40
+ .brand {
41
+ font-weight: 700;
42
+ font-size: 1.2rem;
43
+ }
44
+ .content {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 1.5rem;
48
+ }
49
+ .actions {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 0.75rem;
53
+ }
54
+ ::slotted(a) {
55
+ text-decoration: none;
56
+ color: inherit;
57
+ opacity: 0.8;
58
+ transition: opacity 0.2s;
59
+ }
60
+ ::slotted(a:hover) {
61
+ opacity: 1;
62
+ }
63
+ `);
64
+ // Auto-hide on scroll
65
+ if (this.hasAttribute("auto-hide")) {
66
+ window.addEventListener("scroll", this.handleScroll.bind(this), { passive: true });
67
+ }
68
+ // Scrolled state
69
+ window.addEventListener("scroll", () => {
70
+ this.toggleAttribute("scrolled", window.scrollY > 10);
71
+ }, { passive: true });
72
+ }
73
+ handleScroll() {
74
+ const currentScroll = window.scrollY;
75
+ if (currentScroll > this.lastScroll && currentScroll > 100) {
76
+ this.setAttribute("hidden-nav", "");
77
+ }
78
+ else {
79
+ this.removeAttribute("hidden-nav");
80
+ }
81
+ this.lastScroll = currentScroll;
82
+ }
83
+ }
84
+ customElements.define("glass-navbar", GlassNavbar);
@@ -0,0 +1,12 @@
1
+ import { BaseElement } from "../core/base-element.js";
2
+ export declare class GlassProgress extends BaseElement {
3
+ static get observedAttributes(): string[];
4
+ get value(): number;
5
+ set value(v: number);
6
+ get max(): number;
7
+ connectedCallback(): void;
8
+ private mountLinear;
9
+ private mountCircular;
10
+ private updateProgress;
11
+ attributeChangedCallback(name: string): void;
12
+ }
@@ -0,0 +1,159 @@
1
+ import { BaseElement } from "../core/base-element.js";
2
+ export class GlassProgress extends BaseElement {
3
+ static get observedAttributes() {
4
+ return ["value", "max", "variant", "indeterminate"];
5
+ }
6
+ get value() { return parseFloat(this.getAttribute("value") || "0"); }
7
+ set value(v) { this.setAttribute("value", String(v)); }
8
+ get max() { return parseFloat(this.getAttribute("max") || "100"); }
9
+ connectedCallback() {
10
+ const variant = this.getAttribute("variant") || "linear";
11
+ if (variant === "circular") {
12
+ this.mountCircular();
13
+ }
14
+ else {
15
+ this.mountLinear();
16
+ }
17
+ }
18
+ mountLinear() {
19
+ this.mount(`
20
+ <div class="progress-track" role="progressbar" aria-valuenow="${this.value}" aria-valuemin="0" aria-valuemax="${this.max}">
21
+ <div class="progress-fill"></div>
22
+ </div>
23
+ <span class="progress-label"><slot></slot></span>
24
+ `, `
25
+ :host {
26
+ display: block;
27
+ }
28
+ .progress-track {
29
+ height: 6px;
30
+ background: rgba(255, 255, 255, 0.15);
31
+ border-radius: 999px;
32
+ overflow: hidden;
33
+ }
34
+ .progress-fill {
35
+ height: 100%;
36
+ background: var(--lg-accent);
37
+ border-radius: 999px;
38
+ transition: width 0.3s cubic-bezier(0.16, 1, 0.3, 1);
39
+ }
40
+ :host([indeterminate]) .progress-fill {
41
+ width: 30% !important;
42
+ animation: indeterminate 1.5s ease-in-out infinite;
43
+ }
44
+ .progress-label {
45
+ display: block;
46
+ margin-top: 0.5rem;
47
+ font-size: 0.875rem;
48
+ opacity: 0.7;
49
+ }
50
+ .progress-label:empty {
51
+ display: none;
52
+ }
53
+ @keyframes indeterminate {
54
+ 0% { transform: translateX(-100%); }
55
+ 100% { transform: translateX(400%); }
56
+ }
57
+ `);
58
+ this.updateProgress();
59
+ }
60
+ mountCircular() {
61
+ const size = 80;
62
+ const strokeWidth = 6;
63
+ const radius = (size - strokeWidth) / 2;
64
+ const circumference = 2 * Math.PI * radius;
65
+ this.mount(`
66
+ <div class="circular-wrapper">
67
+ <svg class="circular-progress" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
68
+ <circle class="track" cx="${size / 2}" cy="${size / 2}" r="${radius}" />
69
+ <circle class="fill" cx="${size / 2}" cy="${size / 2}" r="${radius}"
70
+ stroke-dasharray="${circumference}"
71
+ stroke-dashoffset="${circumference}" />
72
+ </svg>
73
+ <span class="circular-label"><slot></slot></span>
74
+ </div>
75
+ `, `
76
+ :host {
77
+ display: inline-block;
78
+ }
79
+ .circular-wrapper {
80
+ position: relative;
81
+ display: inline-flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ }
85
+ .circular-progress {
86
+ transform: rotate(-90deg);
87
+ }
88
+ .track {
89
+ fill: none;
90
+ stroke: rgba(255, 255, 255, 0.15);
91
+ stroke-width: ${strokeWidth};
92
+ }
93
+ .fill {
94
+ fill: none;
95
+ stroke: url(#gradient) currentColor;
96
+ stroke-width: ${strokeWidth};
97
+ stroke-linecap: round;
98
+ transition: stroke-dashoffset 0.3s cubic-bezier(0.16, 1, 0.3, 1);
99
+ }
100
+ :host([indeterminate]) .fill {
101
+ animation: circular-spin 1.5s linear infinite;
102
+ stroke-dashoffset: ${circumference * 0.75} !important;
103
+ }
104
+ :host([indeterminate]) .circular-progress {
105
+ animation: rotate 2s linear infinite;
106
+ }
107
+ .circular-label {
108
+ position: absolute;
109
+ font-size: 1rem;
110
+ font-weight: 600;
111
+ }
112
+ @keyframes circular-spin {
113
+ 0% { stroke-dashoffset: ${circumference}; }
114
+ 50% { stroke-dashoffset: ${circumference * 0.25}; }
115
+ 100% { stroke-dashoffset: ${circumference}; }
116
+ }
117
+ @keyframes rotate {
118
+ 100% { transform: rotate(270deg); }
119
+ }
120
+ `);
121
+ // Add gradient definition
122
+ const svg = this.root.querySelector("svg");
123
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
124
+ defs.innerHTML = `
125
+ <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
126
+ <stop offset="0%" style="stop-color:#5ac8fa" />
127
+ <stop offset="100%" style="stop-color:#007aff" />
128
+ </linearGradient>
129
+ `;
130
+ svg.prepend(defs);
131
+ this.updateProgress();
132
+ }
133
+ updateProgress() {
134
+ const percent = Math.min(100, Math.max(0, (this.value / this.max) * 100));
135
+ const variant = this.getAttribute("variant") || "linear";
136
+ if (variant === "circular") {
137
+ const circle = this.root.querySelector(".fill");
138
+ if (circle) {
139
+ const radius = parseFloat(circle.getAttribute("r") || "37");
140
+ const circumference = 2 * Math.PI * radius;
141
+ const offset = circumference - (percent / 100) * circumference;
142
+ circle.style.strokeDashoffset = String(offset);
143
+ }
144
+ }
145
+ else {
146
+ const fill = this.root.querySelector(".progress-fill");
147
+ if (fill)
148
+ fill.style.width = `${percent}%`;
149
+ }
150
+ const track = this.root.querySelector("[role='progressbar']");
151
+ track === null || track === void 0 ? void 0 : track.setAttribute("aria-valuenow", String(this.value));
152
+ }
153
+ attributeChangedCallback(name) {
154
+ if (name === "value" || name === "max") {
155
+ this.updateProgress();
156
+ }
157
+ }
158
+ }
159
+ customElements.define("glass-progress", GlassProgress);
@@ -0,0 +1,17 @@
1
+ import { BaseElement } from "../core/base-element.js";
2
+ export declare class GlassSlider extends BaseElement {
3
+ private track?;
4
+ private thumb?;
5
+ private fill?;
6
+ private isDragging;
7
+ static get observedAttributes(): string[];
8
+ get min(): number;
9
+ get max(): number;
10
+ get step(): number;
11
+ get value(): number;
12
+ set value(v: number);
13
+ connectedCallback(): void;
14
+ private setupInteraction;
15
+ private updateUI;
16
+ attributeChangedCallback(name: string): void;
17
+ }
@@ -0,0 +1,168 @@
1
+ import { BaseElement } from "../core/base-element.js";
2
+ import { springAnimate, snappySpring } from "../core/spring-engine.js";
3
+ export class GlassSlider extends BaseElement {
4
+ constructor() {
5
+ super(...arguments);
6
+ this.isDragging = false;
7
+ }
8
+ static get observedAttributes() {
9
+ return ["value", "min", "max", "step", "disabled"];
10
+ }
11
+ get min() { return parseFloat(this.getAttribute("min") || "0"); }
12
+ get max() { return parseFloat(this.getAttribute("max") || "100"); }
13
+ get step() { return parseFloat(this.getAttribute("step") || "1"); }
14
+ get value() { return parseFloat(this.getAttribute("value") || "50"); }
15
+ set value(v) { this.setAttribute("value", String(v)); }
16
+ connectedCallback() {
17
+ this.mount(`
18
+ <div class="slider" role="slider" tabindex="0"
19
+ aria-valuemin="${this.min}"
20
+ aria-valuemax="${this.max}"
21
+ aria-valuenow="${this.value}">
22
+ <div class="track">
23
+ <div class="fill"></div>
24
+ </div>
25
+ <div class="thumb"></div>
26
+ </div>
27
+ `, `
28
+ :host {
29
+ display: block;
30
+ padding: 0.5rem 0;
31
+ }
32
+ .slider {
33
+ position: relative;
34
+ height: 24px;
35
+ display: flex;
36
+ align-items: center;
37
+ cursor: pointer;
38
+ }
39
+ .slider:focus-visible .thumb {
40
+ outline: 2px solid var(--lg-accent-focus, #5ac8fa);
41
+ outline-offset: 2px;
42
+ }
43
+ :host([disabled]) .slider {
44
+ opacity: 0.5;
45
+ cursor: not-allowed;
46
+ }
47
+ .track {
48
+ position: absolute;
49
+ left: 0;
50
+ right: 0;
51
+ height: 6px;
52
+ background: rgba(255, 255, 255, 0.15);
53
+ border-radius: 999px;
54
+ overflow: hidden;
55
+ }
56
+ .fill {
57
+ height: 100%;
58
+ background: var(--lg-accent);
59
+ border-radius: 999px;
60
+ transition: width 0.1s ease-out;
61
+ }
62
+ .thumb {
63
+ position: absolute;
64
+ width: 20px;
65
+ height: 20px;
66
+ background: white;
67
+ border-radius: 50%;
68
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
69
+ transform: translateX(-50%);
70
+ transition: transform 0.1s ease;
71
+ }
72
+ .thumb:hover {
73
+ transform: translateX(-50%) scale(1.1);
74
+ }
75
+ .slider:active .thumb {
76
+ transform: translateX(-50%) scale(0.95);
77
+ }
78
+ `);
79
+ this.track = this.root.querySelector(".track");
80
+ this.thumb = this.root.querySelector(".thumb");
81
+ this.fill = this.root.querySelector(".fill");
82
+ this.updateUI(false);
83
+ this.setupInteraction();
84
+ }
85
+ setupInteraction() {
86
+ const slider = this.root.querySelector(".slider");
87
+ const updateFromEvent = (e) => {
88
+ if (this.hasAttribute("disabled"))
89
+ return;
90
+ const rect = this.track.getBoundingClientRect();
91
+ const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
92
+ const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
93
+ const rawValue = this.min + percent * (this.max - this.min);
94
+ const steppedValue = Math.round(rawValue / this.step) * this.step;
95
+ this.value = Math.max(this.min, Math.min(this.max, steppedValue));
96
+ };
97
+ slider.addEventListener("mousedown", (e) => {
98
+ this.isDragging = true;
99
+ updateFromEvent(e);
100
+ });
101
+ slider.addEventListener("touchstart", (e) => {
102
+ this.isDragging = true;
103
+ updateFromEvent(e);
104
+ }, { passive: true });
105
+ window.addEventListener("mousemove", (e) => {
106
+ if (this.isDragging)
107
+ updateFromEvent(e);
108
+ });
109
+ window.addEventListener("touchmove", (e) => {
110
+ if (this.isDragging)
111
+ updateFromEvent(e);
112
+ }, { passive: true });
113
+ window.addEventListener("mouseup", () => this.isDragging = false);
114
+ window.addEventListener("touchend", () => this.isDragging = false);
115
+ // Keyboard support
116
+ slider.addEventListener("keydown", (e) => {
117
+ const key = e.key;
118
+ if (this.hasAttribute("disabled"))
119
+ return;
120
+ let newValue = this.value;
121
+ if (key === "ArrowRight" || key === "ArrowUp") {
122
+ newValue = Math.min(this.max, this.value + this.step);
123
+ }
124
+ else if (key === "ArrowLeft" || key === "ArrowDown") {
125
+ newValue = Math.max(this.min, this.value - this.step);
126
+ }
127
+ else if (key === "Home") {
128
+ newValue = this.min;
129
+ }
130
+ else if (key === "End") {
131
+ newValue = this.max;
132
+ }
133
+ else {
134
+ return;
135
+ }
136
+ e.preventDefault();
137
+ this.value = newValue;
138
+ });
139
+ }
140
+ updateUI(animate = true) {
141
+ const percent = ((this.value - this.min) / (this.max - this.min)) * 100;
142
+ if (this.fill)
143
+ this.fill.style.width = `${percent}%`;
144
+ if (this.thumb) {
145
+ if (animate && !this.isDragging) {
146
+ const currentLeft = parseFloat(this.thumb.style.left) || 0;
147
+ springAnimate(currentLeft, percent, v => {
148
+ this.thumb.style.left = `${v}%`;
149
+ }, snappySpring);
150
+ }
151
+ else {
152
+ this.thumb.style.left = `${percent}%`;
153
+ }
154
+ }
155
+ const slider = this.root.querySelector(".slider");
156
+ slider === null || slider === void 0 ? void 0 : slider.setAttribute("aria-valuenow", String(this.value));
157
+ }
158
+ attributeChangedCallback(name) {
159
+ if (name === "value") {
160
+ this.updateUI();
161
+ this.dispatchEvent(new CustomEvent("input", {
162
+ detail: { value: this.value },
163
+ bubbles: true
164
+ }));
165
+ }
166
+ }
167
+ }
168
+ customElements.define("glass-slider", GlassSlider);
@@ -0,0 +1,7 @@
1
+ import { BaseElement } from "../core/base-element.js";
2
+ export declare class GlassTabs extends BaseElement {
3
+ private indicator?;
4
+ static get observedAttributes(): string[];
5
+ connectedCallback(): void;
6
+ private selectTab;
7
+ }
@@ -0,0 +1,102 @@
1
+ import { BaseElement } from "../core/base-element.js";
2
+ import { springAnimate, snappySpring } from "../core/spring-engine.js";
3
+ export class GlassTabs extends BaseElement {
4
+ static get observedAttributes() {
5
+ return ["value"];
6
+ }
7
+ connectedCallback() {
8
+ this.mount(`
9
+ <div class="tabs-container">
10
+ <div class="indicator"></div>
11
+ <slot></slot>
12
+ </div>
13
+ `, `
14
+ :host {
15
+ display: inline-block;
16
+ }
17
+ .tabs-container {
18
+ position: relative;
19
+ display: flex;
20
+ gap: 0.25rem;
21
+ padding: 0.25rem;
22
+ background: var(--lg-bg);
23
+ backdrop-filter: blur(var(--lg-blur));
24
+ -webkit-backdrop-filter: blur(var(--lg-blur));
25
+ border-radius: var(--lg-radius);
26
+ border: 1px solid var(--lg-border);
27
+ }
28
+ .indicator {
29
+ position: absolute;
30
+ top: 0.25rem;
31
+ bottom: 0.25rem;
32
+ background: rgba(255, 255, 255, 0.15);
33
+ border-radius: calc(var(--lg-radius) - 4px);
34
+ transition: none;
35
+ pointer-events: none;
36
+ }
37
+ ::slotted(button) {
38
+ position: relative;
39
+ padding: 0.6rem 1.2rem;
40
+ border: none;
41
+ background: transparent;
42
+ color: inherit;
43
+ font: inherit;
44
+ cursor: pointer;
45
+ border-radius: calc(var(--lg-radius) - 4px);
46
+ opacity: 0.7;
47
+ transition: opacity 0.2s;
48
+ z-index: 1;
49
+ }
50
+ ::slotted(button:hover) {
51
+ opacity: 1;
52
+ }
53
+ ::slotted(button[aria-selected="true"]) {
54
+ opacity: 1;
55
+ }
56
+ `);
57
+ this.indicator = this.root.querySelector(".indicator");
58
+ // Handle tab clicks
59
+ this.addEventListener("click", (e) => {
60
+ const target = e.target;
61
+ if (target.tagName === "BUTTON") {
62
+ this.selectTab(target);
63
+ }
64
+ });
65
+ // Initialize first tab
66
+ requestAnimationFrame(() => {
67
+ const firstTab = this.querySelector("button");
68
+ if (firstTab)
69
+ this.selectTab(firstTab, false);
70
+ });
71
+ }
72
+ selectTab(tab, animate = true) {
73
+ // Update aria states
74
+ this.querySelectorAll("button").forEach(btn => btn.setAttribute("aria-selected", "false"));
75
+ tab.setAttribute("aria-selected", "true");
76
+ // Animate indicator
77
+ const rect = tab.getBoundingClientRect();
78
+ const containerRect = this.getBoundingClientRect();
79
+ const left = rect.left - containerRect.left - 4; // Subtract padding
80
+ const width = rect.width;
81
+ if (animate && this.indicator) {
82
+ const currentLeft = parseFloat(this.indicator.style.left) || 0;
83
+ const currentWidth = parseFloat(this.indicator.style.width) || width;
84
+ springAnimate(currentLeft, left, v => {
85
+ this.indicator.style.left = `${v}px`;
86
+ }, snappySpring);
87
+ springAnimate(currentWidth, width, v => {
88
+ this.indicator.style.width = `${v}px`;
89
+ }, snappySpring);
90
+ }
91
+ else if (this.indicator) {
92
+ this.indicator.style.left = `${left}px`;
93
+ this.indicator.style.width = `${width}px`;
94
+ }
95
+ // Dispatch event
96
+ this.dispatchEvent(new CustomEvent("change", {
97
+ detail: { value: tab.dataset.value || tab.textContent },
98
+ bubbles: true
99
+ }));
100
+ }
101
+ }
102
+ customElements.define("glass-tabs", GlassTabs);