@pol-cova/gessi 0.0.1 → 0.0.2

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.
@@ -0,0 +1,953 @@
1
+ const HTMLElementBase = globalThis.HTMLElement ?? class {};
2
+ const focusableSelector = [
3
+ "a[href]",
4
+ "button:not([disabled])",
5
+ "input:not([disabled])",
6
+ "select:not([disabled])",
7
+ "textarea:not([disabled])",
8
+ "[tabindex]:not([tabindex='-1'])",
9
+ ].join(",");
10
+
11
+ const cssLength = (value, fallback) => {
12
+ if (!value) return fallback;
13
+ return /^-?\d+(\.\d+)?$/.test(value) ? `${value}px` : value;
14
+ };
15
+
16
+ const mediaFilters = {
17
+ mono: "grayscale(1) contrast(1.25)",
18
+ posterize: "contrast(1.8) saturate(.75) brightness(1.08)",
19
+ dither: "grayscale(1) contrast(2.25)",
20
+ halftone: "grayscale(1) contrast(1.35) brightness(1.2)",
21
+ duotone: "grayscale(1) contrast(1.35)",
22
+ sepia: "sepia(.92) saturate(.85) contrast(1.12)",
23
+ invert: "invert(1)",
24
+ noir: "grayscale(1) contrast(1.65) brightness(.86)",
25
+ xray: "grayscale(1) invert(1) contrast(1.5)",
26
+ chromatic: "saturate(1.45) contrast(1.1) drop-shadow(-3px 0 0 rgb(255 0 72 / 65%)) drop-shadow(3px 0 0 rgb(0 218 255 / 65%))",
27
+ blueprint: "grayscale(1) contrast(2.2) invert(1)",
28
+ thermal: "saturate(4) contrast(1.5) hue-rotate(155deg)",
29
+ dream: "saturate(1.35) contrast(.9) brightness(1.12)",
30
+ comic: "saturate(1.8) contrast(1.45)",
31
+ glitch: "contrast(1.35) saturate(1.4) drop-shadow(4px 0 0 rgb(255 0 72 / 55%)) drop-shadow(-4px 0 0 rgb(0 226 255 / 55%))",
32
+ };
33
+
34
+ const applyMediaEffects = (image, effects) => {
35
+ const filter = effects
36
+ .split(/\s+/)
37
+ .map((effect) => mediaFilters[effect])
38
+ .filter(Boolean)
39
+ .join(" ");
40
+ if (filter) image.style.filter = filter;
41
+ };
42
+
43
+ const makeButton = (label, symbol, action) => {
44
+ const button = document.createElement("button");
45
+ button.className = "gs-window-control";
46
+ button.type = "button";
47
+ button.ariaLabel = label;
48
+ button.dataset.action = action;
49
+ button.textContent = symbol;
50
+ return button;
51
+ };
52
+
53
+ class GessiDesktop extends HTMLElementBase {
54
+ #topLayer = 10;
55
+
56
+ connectedCallback() {
57
+ if (this.dataset.enhanced) return;
58
+ this.dataset.enhanced = "true";
59
+ const theme = this.getAttribute("theme") || "old-tech";
60
+ this.dataset.gsStyle ||= theme;
61
+ this.classList.add("gs-desktop");
62
+ this.tabIndex ||= -1;
63
+
64
+ for (const [attribute, property] of [
65
+ ["background", "--gs-desktop-color"],
66
+ ["pattern-color", "--gs-pattern-color"],
67
+ ["pattern-size", "--gs-pattern-size"],
68
+ ]) {
69
+ if (this.hasAttribute(attribute)) {
70
+ this.style.setProperty(property, this.getAttribute(attribute));
71
+ }
72
+ }
73
+ if (!this.hasAttribute("background")) {
74
+ this.style.setProperty(
75
+ "--gs-desktop-color",
76
+ theme === "classic-os" ? "#fff" : "#9edbd2",
77
+ );
78
+ }
79
+ this.dataset.pattern = this.getAttribute("pattern")
80
+ || (theme === "classic-os" ? "checker" : "dots");
81
+
82
+ const customMenu = this.querySelector(':scope > [slot="menu"]');
83
+ const menu = this.getAttribute("menu");
84
+ if (customMenu) {
85
+ customMenu.removeAttribute("slot");
86
+ customMenu.classList.add("gs-menubar");
87
+ customMenu.ariaLabel ||= "Desktop menu";
88
+ this.prepend(customMenu);
89
+ } else if (menu && !this.querySelector(":scope > .gs-menubar")) {
90
+ const bar = document.createElement("nav");
91
+ bar.className = "gs-menubar";
92
+ bar.ariaLabel = "Desktop menu";
93
+
94
+ menu.split(",").map((item) => item.trim()).filter(Boolean).forEach((item, index) => {
95
+ const entry = document.createElement(index === 0 ? "strong" : "span");
96
+ entry.textContent = item;
97
+ bar.append(entry);
98
+ });
99
+
100
+ const clock = document.createElement("span");
101
+ clock.className = "gs-menubar-end";
102
+ clock.textContent = this.getAttribute("clock") || "";
103
+ bar.append(clock);
104
+ this.prepend(bar);
105
+ }
106
+
107
+ this.addEventListener("gs-window-focus", (event) => {
108
+ this.querySelectorAll("gessi-window, gessi-dialog").forEach((window) => {
109
+ window.toggleAttribute("active", window === event.target);
110
+ });
111
+ event.target.style.setProperty("--gs-window-layer", String(++this.#topLayer));
112
+ });
113
+ this.addEventListener("keydown", (event) => this.#handleKeyboard(event));
114
+ this.addEventListener("click", (event) => {
115
+ const patternControl = event.target.closest("[data-gessi-pattern]");
116
+ const backgroundControl = event.target.closest("[data-gessi-background]");
117
+ if (patternControl) this.dataset.pattern = patternControl.dataset.gessiPattern;
118
+ if (backgroundControl) {
119
+ this.style.setProperty("--gs-desktop-color", backgroundControl.dataset.gessiBackground);
120
+ }
121
+ });
122
+
123
+ queueMicrotask(() => {
124
+ const activeWindow = this.querySelector("gessi-window[active], gessi-dialog[active]");
125
+ if (activeWindow) activeWindow.focusWindow();
126
+ });
127
+ }
128
+
129
+ #handleKeyboard(event) {
130
+ const windows = [...this.querySelectorAll("gessi-window:not([hidden]), gessi-dialog:not([hidden])")];
131
+ if (!windows.length) return;
132
+
133
+ if (event.key === "F6") {
134
+ event.preventDefault();
135
+ const activeIndex = windows.findIndex((window) => window.hasAttribute("active"));
136
+ const direction = event.shiftKey ? -1 : 1;
137
+ const nextIndex = (activeIndex + direction + windows.length) % windows.length;
138
+ windows[nextIndex].focusWindow();
139
+ return;
140
+ }
141
+
142
+ if (!event.ctrlKey || !event.shiftKey) return;
143
+ const activeWindow = windows.find((window) => window.hasAttribute("active"));
144
+ if (!activeWindow) return;
145
+
146
+ const step = event.altKey ? 1 : 8;
147
+ const keyMoves = {
148
+ ArrowLeft: [-step, 0],
149
+ ArrowRight: [step, 0],
150
+ ArrowUp: [0, -step],
151
+ ArrowDown: [0, step],
152
+ };
153
+ if (keyMoves[event.key]) {
154
+ event.preventDefault();
155
+ activeWindow.moveBy(...keyMoves[event.key]);
156
+ }
157
+ if (event.key === "+" || event.key === "=") {
158
+ event.preventDefault();
159
+ activeWindow.resizeBy(step, step);
160
+ }
161
+ if (event.key === "-") {
162
+ event.preventDefault();
163
+ activeWindow.resizeBy(-step, -step);
164
+ }
165
+ }
166
+ }
167
+
168
+ class GessiWindow extends HTMLElementBase {
169
+ #dragCleanup;
170
+ #returnFocus;
171
+
172
+ connectedCallback() {
173
+ if (this.dataset.enhanced) return;
174
+ this.dataset.enhanced = "true";
175
+ this.classList.add("gs-window");
176
+ this.tabIndex ||= -1;
177
+ if (this.parentElement?.closest("gessi-window")) {
178
+ this.setAttribute("contained", "");
179
+ }
180
+
181
+ for (const [attribute, property, fallback, transform = cssLength] of [
182
+ ["x", "--gs-window-x", "auto"],
183
+ ["y", "--gs-window-y", "auto"],
184
+ ["width", "--gs-window-width", "34rem"],
185
+ ["height", "--gs-window-min-height", "12rem"],
186
+ ["padding", "--gs-window-pad", "1rem"],
187
+ ["layer", "--gs-window-layer", "1", (value, defaultValue) => value || defaultValue],
188
+ ]) {
189
+ this.style.setProperty(
190
+ property,
191
+ transform(this.getAttribute(attribute), fallback),
192
+ );
193
+ }
194
+
195
+ const nodes = [...this.childNodes];
196
+ const toolbarNodes = nodes.filter((node) => node.nodeType === 1 && node.getAttribute("slot") === "toolbar");
197
+ const sidebarNodes = nodes.filter((node) => node.nodeType === 1 && node.getAttribute("slot") === "sidebar");
198
+ const statusNodes = nodes.filter((node) => node.nodeType === 1 && node.getAttribute("slot") === "status");
199
+ const contentNodes = nodes.filter((node) => !toolbarNodes.includes(node) && !sidebarNodes.includes(node) && !statusNodes.includes(node));
200
+
201
+ this.replaceChildren();
202
+ this.append(this.#makeTitlebar());
203
+
204
+ if (toolbarNodes.length) {
205
+ const toolbar = document.createElement("div");
206
+ toolbar.className = "gs-window-toolbar";
207
+ toolbar.append(...toolbarNodes);
208
+ this.append(toolbar);
209
+ }
210
+
211
+ const body = document.createElement("div");
212
+ body.className = "gs-window-body";
213
+
214
+ if (sidebarNodes.length) {
215
+ const sidebar = document.createElement("aside");
216
+ sidebar.className = "gs-window-sidebar";
217
+ sidebar.append(...sidebarNodes);
218
+ body.append(sidebar);
219
+ }
220
+
221
+ const content = document.createElement("div");
222
+ content.className = "gs-window-content";
223
+ content.append(...contentNodes);
224
+ body.append(content);
225
+ this.append(body);
226
+
227
+ if (statusNodes.length || this.hasAttribute("status")) {
228
+ const status = document.createElement("footer");
229
+ status.className = "gs-statusbar";
230
+ status.append(...statusNodes);
231
+ if (!statusNodes.length) status.textContent = this.getAttribute("status");
232
+ this.append(status);
233
+ }
234
+
235
+ if (this.hasAttribute("dialog")) {
236
+ this.classList.add("gs-dialog");
237
+ this.role = "dialog";
238
+ this.ariaModal = "true";
239
+ }
240
+
241
+ this.toggleAttribute("active", this.hasAttribute("active"));
242
+ this.addEventListener("pointerdown", (event) => {
243
+ if (event.target.closest(".gs-window") === this) this.focusWindow();
244
+ });
245
+ this.addEventListener("click", (event) => this.#handleControl(event));
246
+ this.addEventListener("keydown", (event) => {
247
+ if (!this.hasAttribute("dialog")) return;
248
+ if (event.key === "Escape") this.close();
249
+ if (event.key === "Tab") this.#trapFocus(event);
250
+ });
251
+ this.#enableDragging();
252
+ }
253
+
254
+ disconnectedCallback() {
255
+ this.#dragCleanup?.();
256
+ }
257
+
258
+ focusWindow() {
259
+ this.dispatchEvent(new CustomEvent("gs-window-focus", { bubbles: true }));
260
+ this.focus({ preventScroll: true });
261
+ }
262
+
263
+ open() {
264
+ this.#returnFocus = document.activeElement;
265
+ this.hidden = false;
266
+ this.focusWindow();
267
+ this.dispatchEvent(new CustomEvent("gs-open"));
268
+ }
269
+
270
+ close() {
271
+ this.hidden = true;
272
+ this.#returnFocus?.focus?.();
273
+ this.dispatchEvent(new CustomEvent("gs-close"));
274
+ }
275
+
276
+ moveBy(x, y) {
277
+ const rect = this.getBoundingClientRect();
278
+ const boundary = this.#dragBoundary();
279
+ const boundaryRect = boundary?.getBoundingClientRect();
280
+ const nextX = Math.max(
281
+ 0,
282
+ Math.min(
283
+ rect.left - (boundaryRect?.left || 0) + x,
284
+ Math.max(0, (boundaryRect?.width || rect.width) - rect.width),
285
+ ),
286
+ );
287
+ const nextY = Math.max(
288
+ 0,
289
+ Math.min(
290
+ rect.top - (boundaryRect?.top || 0) + y,
291
+ Math.max(0, (boundaryRect?.height || rect.height) - rect.height),
292
+ ),
293
+ );
294
+ this.style.setProperty("--gs-window-x", `${nextX}px`);
295
+ this.style.setProperty("--gs-window-y", `${nextY}px`);
296
+ }
297
+
298
+ resizeBy(width, height) {
299
+ const rect = this.getBoundingClientRect();
300
+ this.style.setProperty("--gs-window-width", `${Math.max(160, rect.width + width)}px`);
301
+ this.style.setProperty("--gs-window-min-height", `${Math.max(80, rect.height + height)}px`);
302
+ }
303
+
304
+ #makeTitlebar() {
305
+ const bar = document.createElement("header");
306
+ bar.className = "gs-window-titlebar";
307
+
308
+ const start = document.createElement("div");
309
+ start.className = "gs-window-controls";
310
+ if (!this.hasAttribute("no-close")) start.append(makeButton("Close", "×", "close"));
311
+
312
+ const title = document.createElement("strong");
313
+ title.className = "gs-window-title";
314
+ title.textContent = this.getAttribute("title") || "Untitled";
315
+
316
+ const end = document.createElement("div");
317
+ end.className = "gs-window-controls";
318
+ if (this.hasAttribute("minimizable")) end.append(makeButton("Minimize", "–", "minimize"));
319
+ if (this.hasAttribute("zoomable")) end.append(makeButton("Zoom", "□", "zoom"));
320
+
321
+ bar.append(start, title, end);
322
+ return bar;
323
+ }
324
+
325
+ #handleControl(event) {
326
+ const control = event.target.closest("[data-action]");
327
+ if (!control) return;
328
+
329
+ const action = control.dataset.action;
330
+ if (action === "close") this.close();
331
+ if (action === "minimize") this.toggleAttribute("minimized");
332
+ if (action === "zoom") this.toggleAttribute("maximized");
333
+ }
334
+
335
+ #trapFocus(event) {
336
+ const focusable = [...this.querySelectorAll(focusableSelector)]
337
+ .filter((element) => !element.hidden && element.offsetParent !== null);
338
+ if (!focusable.length) {
339
+ event.preventDefault();
340
+ this.focus();
341
+ return;
342
+ }
343
+ const first = focusable[0];
344
+ const last = focusable[focusable.length - 1];
345
+ if (event.shiftKey && document.activeElement === first) {
346
+ event.preventDefault();
347
+ last.focus();
348
+ } else if (!event.shiftKey && document.activeElement === last) {
349
+ event.preventDefault();
350
+ first.focus();
351
+ }
352
+ }
353
+
354
+ #enableDragging() {
355
+ if (
356
+ !this.hasAttribute("draggable")
357
+ || globalThis.matchMedia?.("(max-width: 719px)").matches
358
+ ) return;
359
+ const handle = this.querySelector(".gs-window-titlebar");
360
+
361
+ const onPointerDown = (event) => {
362
+ if (event.target.closest("button")) return;
363
+ event.preventDefault();
364
+ this.focusWindow();
365
+
366
+ const boundary = this.#dragBoundary();
367
+ const desktopRect = boundary.getBoundingClientRect();
368
+ const windowRect = this.getBoundingClientRect();
369
+ const offsetX = event.clientX - windowRect.left;
370
+ const offsetY = event.clientY - windowRect.top;
371
+ const onPointerMove = (moveEvent) => {
372
+ const x = Math.max(0, Math.min(moveEvent.clientX - desktopRect.left - offsetX, desktopRect.width - windowRect.width));
373
+ const y = Math.max(
374
+ 0,
375
+ Math.min(
376
+ moveEvent.clientY - desktopRect.top - offsetY,
377
+ desktopRect.height - windowRect.height,
378
+ ),
379
+ );
380
+ this.style.setProperty("--gs-window-x", `${x}px`);
381
+ this.style.setProperty("--gs-window-y", `${y}px`);
382
+ };
383
+
384
+ const onPointerUp = () => {
385
+ document.removeEventListener("pointermove", onPointerMove);
386
+ document.removeEventListener("pointerup", onPointerUp);
387
+ document.removeEventListener("pointercancel", onPointerUp);
388
+ };
389
+
390
+ document.addEventListener("pointermove", onPointerMove);
391
+ document.addEventListener("pointerup", onPointerUp);
392
+ document.addEventListener("pointercancel", onPointerUp);
393
+ };
394
+
395
+ handle.addEventListener("pointerdown", onPointerDown);
396
+ this.#dragCleanup = () => handle.removeEventListener("pointerdown", onPointerDown);
397
+ }
398
+
399
+ #dragBoundary() {
400
+ if (this.hasAttribute("contained")) {
401
+ return this.parentElement?.closest(".gs-window-content")
402
+ || this.parentElement;
403
+ }
404
+ return this.closest("gessi-desktop, .gs-desktop");
405
+ }
406
+ }
407
+
408
+ class GessiIcon extends HTMLElementBase {
409
+ connectedCallback() {
410
+ if (this.dataset.enhanced) return;
411
+ this.dataset.enhanced = "true";
412
+ this.classList.add("gs-desktop-icon");
413
+ if (this.hasAttribute("x") || this.hasAttribute("y")) {
414
+ this.style.position = "absolute";
415
+ this.style.left = cssLength(this.getAttribute("x"), "auto");
416
+ this.style.top = cssLength(this.getAttribute("y"), "auto");
417
+ }
418
+
419
+ const href = this.getAttribute("href");
420
+ const action = this.getAttribute("action");
421
+ const interactive = href || action;
422
+ const surface = document.createElement(href ? "a" : action ? "button" : "span");
423
+ surface.className = "gs-icon-action";
424
+ if (href) {
425
+ surface.href = href;
426
+ for (const attribute of ["target", "rel", "download"]) {
427
+ if (this.hasAttribute(attribute)) {
428
+ surface.setAttribute(attribute, this.getAttribute(attribute));
429
+ }
430
+ }
431
+ }
432
+ if (action) {
433
+ surface.type = "button";
434
+ surface.addEventListener("click", () => this.#runAction(action));
435
+ }
436
+
437
+ const icon = document.createElement("span");
438
+ icon.className = "gs-icon";
439
+ icon.ariaHidden = "true";
440
+ if (this.hasAttribute("src")) {
441
+ const image = document.createElement("img");
442
+ image.src = this.getAttribute("src");
443
+ image.alt = "";
444
+ icon.append(image);
445
+ } else {
446
+ icon.textContent = this.getAttribute("icon") || "◇";
447
+ }
448
+
449
+ const label = document.createElement("span");
450
+ label.textContent = this.getAttribute("label") || this.textContent.trim();
451
+ surface.append(icon, label);
452
+ this.replaceChildren(surface);
453
+ if (interactive) this.dataset.interactive = "true";
454
+ }
455
+
456
+ #runAction(action) {
457
+ const [command, selector] = action.split(":", 2);
458
+ const target = selector ? document.querySelector(selector) : null;
459
+ if (command === "open") target?.open?.();
460
+ if (command === "close") target?.close?.();
461
+ this.dispatchEvent(new CustomEvent("gs-icon-activate", {
462
+ bubbles: true,
463
+ detail: { action, command, target },
464
+ }));
465
+ }
466
+ }
467
+
468
+ class GessiIcons extends HTMLElementBase {
469
+ connectedCallback() {
470
+ if (this.dataset.enhanced) return;
471
+ this.dataset.enhanced = "true";
472
+ this.classList.add(this.hasAttribute("desktop") ? "gs-desktop-icons" : "gs-icon-grid");
473
+ if (this.getAttribute("side") === "left") this.classList.add("gs-left");
474
+ if (this.getAttribute("side") === "right") this.classList.add("gs-right");
475
+ }
476
+ }
477
+
478
+ class GessiDialog extends GessiWindow {
479
+ connectedCallback() {
480
+ this.setAttribute("dialog", "");
481
+ this.setAttribute("active", "");
482
+ super.connectedCallback();
483
+ }
484
+ }
485
+
486
+ class GessiTabs extends HTMLElementBase {
487
+ connectedCallback() {
488
+ if (this.dataset.enhanced) return;
489
+ this.dataset.enhanced = "true";
490
+ this.classList.add("gs-tabs");
491
+ this.setAttribute("role", "tablist");
492
+ this.querySelectorAll(":scope > a, :scope > button").forEach((tab) => {
493
+ tab.setAttribute("role", "tab");
494
+ tab.setAttribute("aria-selected", String(tab.hasAttribute("active")));
495
+ });
496
+ }
497
+ }
498
+
499
+ class GessiPanel extends HTMLElementBase {
500
+ connectedCallback() {
501
+ if (this.dataset.enhanced) return;
502
+ this.dataset.enhanced = "true";
503
+ this.classList.add("gs-control-panel");
504
+ this.style.setProperty(
505
+ "--gs-panel-columns",
506
+ this.getAttribute("columns") || "1",
507
+ );
508
+ this.style.setProperty(
509
+ "--gs-panel-gap",
510
+ cssLength(this.getAttribute("gap"), "0.75rem"),
511
+ );
512
+ if (this.hasAttribute("title")) {
513
+ const heading = document.createElement("strong");
514
+ heading.className = "gs-control-panel-title";
515
+ heading.textContent = this.getAttribute("title");
516
+ this.prepend(heading);
517
+ }
518
+ }
519
+ }
520
+
521
+ class GessiMeter extends HTMLElementBase {
522
+ connectedCallback() {
523
+ if (this.dataset.enhanced) return;
524
+ this.dataset.enhanced = "true";
525
+ this.classList.add("gs-meter");
526
+ const value = Math.max(0, Math.min(100, Number(this.getAttribute("value") || 0)));
527
+ const labelText = this.getAttribute("label") || this.textContent.trim();
528
+ const label = document.createElement("span");
529
+ label.textContent = labelText;
530
+ const track = document.createElement("span");
531
+ track.className = "gs-meter-track";
532
+ track.setAttribute("role", "progressbar");
533
+ track.setAttribute("aria-label", labelText || "Progress");
534
+ track.setAttribute("aria-valuemin", "0");
535
+ track.setAttribute("aria-valuemax", "100");
536
+ track.setAttribute("aria-valuenow", String(value));
537
+ const fill = document.createElement("span");
538
+ fill.style.width = `${value}%`;
539
+ track.append(fill);
540
+ const output = document.createElement("output");
541
+ output.textContent = `${value}%`;
542
+ this.replaceChildren(label, track, output);
543
+ }
544
+ }
545
+
546
+ class GessiList extends HTMLElementBase {
547
+ connectedCallback() {
548
+ if (this.dataset.enhanced) return;
549
+ this.dataset.enhanced = "true";
550
+ this.classList.add("gs-listbox");
551
+ this.setAttribute("role", this.getAttribute("role") || "listbox");
552
+ this.querySelectorAll(":scope > a, :scope > button, :scope > label").forEach((item) => {
553
+ item.setAttribute("role", "option");
554
+ item.setAttribute("aria-selected", String(item.hasAttribute("selected")));
555
+ });
556
+ }
557
+ }
558
+
559
+ class GessiAlert extends HTMLElementBase {
560
+ connectedCallback() {
561
+ if (this.dataset.enhanced) return;
562
+ this.dataset.enhanced = "true";
563
+ this.classList.add("gs-alert");
564
+ this.setAttribute("role", this.getAttribute("role") || "alert");
565
+ if (this.hasAttribute("title")) {
566
+ const title = document.createElement("strong");
567
+ title.className = "gs-alert-title";
568
+ title.textContent = this.getAttribute("title");
569
+ this.prepend(title);
570
+ }
571
+ if (this.hasAttribute("dismissible")) {
572
+ const close = makeButton("Dismiss", "×", "dismiss");
573
+ close.classList.add("gs-alert-close");
574
+ close.addEventListener("click", () => {
575
+ this.hidden = true;
576
+ this.dispatchEvent(new CustomEvent("gs-dismiss"));
577
+ });
578
+ this.prepend(close);
579
+ }
580
+ }
581
+ }
582
+
583
+ class GessiToolbar extends HTMLElementBase {
584
+ connectedCallback() {
585
+ if (this.dataset.enhanced) return;
586
+ this.dataset.enhanced = "true";
587
+ this.classList.add("gs-commandbar");
588
+ this.setAttribute("role", this.getAttribute("role") || "toolbar");
589
+ this.ariaLabel ||= this.getAttribute("label") || "Commands";
590
+ }
591
+ }
592
+
593
+ class GessiDock extends HTMLElementBase {
594
+ connectedCallback() {
595
+ if (this.dataset.enhanced) return;
596
+ this.dataset.enhanced = "true";
597
+ this.classList.add("gs-dock");
598
+ this.dataset.position = this.getAttribute("position") || "bottom";
599
+ this.setAttribute("role", this.getAttribute("role") || "toolbar");
600
+ this.ariaLabel ||= this.getAttribute("label") || "Applications";
601
+ }
602
+ }
603
+
604
+ class GessiMenu extends HTMLElementBase {
605
+ #outsideCleanup;
606
+
607
+ connectedCallback() {
608
+ if (this.dataset.enhanced) return;
609
+ this.dataset.enhanced = "true";
610
+ this.classList.add("gs-menu");
611
+
612
+ const label = this.getAttribute("label");
613
+ if (!label) {
614
+ this.classList.add("gs-menu-list");
615
+ this.setAttribute("role", this.getAttribute("role") || "menu");
616
+ this.#prepareItems(this);
617
+ return;
618
+ }
619
+
620
+ const items = [...this.childNodes];
621
+ const button = document.createElement("button");
622
+ const panel = document.createElement("div");
623
+ const id = this.id || `gs-menu-${Math.random().toString(36).slice(2, 9)}`;
624
+ this.id ||= id;
625
+ panel.id = `${id}-panel`;
626
+ button.type = "button";
627
+ button.className = "gs-menu-trigger";
628
+ button.textContent = label;
629
+ button.setAttribute("aria-haspopup", "menu");
630
+ button.setAttribute("aria-controls", panel.id);
631
+ button.setAttribute("aria-expanded", "false");
632
+ panel.className = "gs-menu-panel";
633
+ panel.setAttribute("role", "menu");
634
+ panel.hidden = true;
635
+ panel.append(...items);
636
+ this.#prepareItems(panel);
637
+ this.replaceChildren(button, panel);
638
+
639
+ const close = (restoreFocus = false) => {
640
+ panel.hidden = true;
641
+ button.setAttribute("aria-expanded", "false");
642
+ this.removeAttribute("open");
643
+ if (restoreFocus) button.focus();
644
+ };
645
+ const open = () => {
646
+ panel.hidden = false;
647
+ button.setAttribute("aria-expanded", "true");
648
+ this.setAttribute("open", "");
649
+ panel.querySelector(focusableSelector)?.focus();
650
+ };
651
+
652
+ button.addEventListener("click", () => panel.hidden ? open() : close());
653
+ this.addEventListener("keydown", (event) => {
654
+ if (event.key === "Escape") close(true);
655
+ if (event.key === "ArrowDown" && document.activeElement === button) {
656
+ event.preventDefault();
657
+ open();
658
+ }
659
+ });
660
+ const onOutsidePointer = (event) => {
661
+ if (!this.contains(event.target)) close();
662
+ };
663
+ document.addEventListener("pointerdown", onOutsidePointer);
664
+ this.#outsideCleanup = () => document.removeEventListener("pointerdown", onOutsidePointer);
665
+ }
666
+
667
+ disconnectedCallback() {
668
+ this.#outsideCleanup?.();
669
+ }
670
+
671
+ #prepareItems(container) {
672
+ container.querySelectorAll(":scope > a, :scope > button").forEach((item) => {
673
+ item.setAttribute("role", "menuitem");
674
+ });
675
+ }
676
+ }
677
+
678
+ class GessiBreadcrumb extends HTMLElementBase {
679
+ connectedCallback() {
680
+ if (this.dataset.enhanced) return;
681
+ this.dataset.enhanced = "true";
682
+ this.classList.add("gs-breadcrumb");
683
+ this.setAttribute("role", "navigation");
684
+ this.ariaLabel ||= this.getAttribute("label") || "Breadcrumb";
685
+ const links = [...this.querySelectorAll("a")];
686
+ links.at(-1)?.setAttribute("aria-current", "page");
687
+ }
688
+ }
689
+
690
+ class GessiTree extends HTMLElementBase {
691
+ connectedCallback() {
692
+ if (this.dataset.enhanced) return;
693
+ this.dataset.enhanced = "true";
694
+ this.classList.add("gs-tree");
695
+ this.setAttribute("role", "tree");
696
+ this.querySelectorAll("li").forEach((item) => item.setAttribute("role", "treeitem"));
697
+ this.querySelectorAll("ul").forEach((group) => group.setAttribute("role", "group"));
698
+ }
699
+ }
700
+
701
+ class GessiSeparator extends HTMLElementBase {
702
+ connectedCallback() {
703
+ if (this.dataset.enhanced) return;
704
+ this.dataset.enhanced = "true";
705
+ this.classList.add("gs-separator");
706
+ const orientation = this.getAttribute("orientation") || "horizontal";
707
+ this.dataset.orientation = orientation;
708
+ this.setAttribute("role", "separator");
709
+ this.setAttribute("aria-orientation", orientation);
710
+ }
711
+ }
712
+
713
+ class GessiTooltip extends HTMLElementBase {
714
+ connectedCallback() {
715
+ if (this.dataset.enhanced) return;
716
+ this.dataset.enhanced = "true";
717
+ this.classList.add("gs-tooltip");
718
+ const tip = document.createElement("span");
719
+ const id = this.id || `gs-tooltip-${Math.random().toString(36).slice(2, 9)}`;
720
+ this.id ||= id;
721
+ tip.id = `${id}-tip`;
722
+ tip.className = "gs-tooltip-content";
723
+ tip.setAttribute("role", "tooltip");
724
+ tip.textContent = this.getAttribute("text") || this.getAttribute("label") || "";
725
+ this.firstElementChild?.setAttribute("aria-describedby", tip.id);
726
+ this.append(tip);
727
+ }
728
+ }
729
+
730
+ class GessiToast extends GessiAlert {
731
+ connectedCallback() {
732
+ if (!this.hasAttribute("role")) this.setAttribute("role", "status");
733
+ super.connectedCallback();
734
+ this.classList.add("gs-toast");
735
+ this.dataset.position = this.getAttribute("position") || "bottom-right";
736
+ }
737
+ }
738
+
739
+ class GessiMedia extends HTMLElementBase {
740
+ connectedCallback() {
741
+ if (this.dataset.enhanced) return;
742
+ this.dataset.enhanced = "true";
743
+ this.classList.add("gs-media-frame");
744
+ this.dataset.effect = this.getAttribute("effect") || "none";
745
+ this.dataset.frame = this.getAttribute("frame") || "window";
746
+ this.style.setProperty("--gs-media-aspect", this.getAttribute("aspect") || "auto");
747
+ this.style.setProperty("--gs-media-fit", this.getAttribute("fit") || "cover");
748
+ this.style.setProperty("--gs-media-position", this.getAttribute("position") || "center");
749
+
750
+ const figure = document.createElement("figure");
751
+ const image = document.createElement("img");
752
+ image.src = this.getAttribute("src") || "";
753
+ image.alt = this.getAttribute("alt") || "";
754
+ image.loading = this.getAttribute("loading") || "lazy";
755
+ image.decoding = "async";
756
+ applyMediaEffects(image, this.dataset.effect);
757
+ image.style.objectPosition = this.getAttribute("position") || "center";
758
+ if (this.hasAttribute("width")) image.width = Number(this.getAttribute("width"));
759
+ if (this.hasAttribute("height")) image.height = Number(this.getAttribute("height"));
760
+
761
+ const viewport = document.createElement("span");
762
+ viewport.className = "gs-media-viewport";
763
+ viewport.append(image);
764
+ figure.append(viewport);
765
+
766
+ const captionText = this.getAttribute("caption");
767
+ if (captionText) {
768
+ const caption = document.createElement("figcaption");
769
+ caption.textContent = captionText;
770
+ figure.append(caption);
771
+ }
772
+
773
+ this.replaceChildren(figure);
774
+ if (this.hasAttribute("zoomable")) {
775
+ this.tabIndex = 0;
776
+ this.setAttribute("role", "button");
777
+ this.setAttribute("aria-label", `Zoom ${image.alt || "image"}`);
778
+ const toggle = () => this.toggleAttribute("expanded");
779
+ this.addEventListener("click", toggle);
780
+ this.addEventListener("keydown", (event) => {
781
+ if (event.key === "Enter" || event.key === " ") {
782
+ event.preventDefault();
783
+ toggle();
784
+ }
785
+ if (event.key === "Escape") this.removeAttribute("expanded");
786
+ });
787
+ }
788
+ }
789
+ }
790
+
791
+ class GessiMarker extends HTMLElementBase {
792
+ connectedCallback() {
793
+ if (this.dataset.enhanced) return;
794
+ this.dataset.enhanced = "true";
795
+ this.classList.add("gs-map-marker");
796
+ this.style.left = this.getAttribute("x") || "50%";
797
+ this.style.top = this.getAttribute("y") || "50%";
798
+ this.setAttribute("role", "img");
799
+ this.ariaLabel = this.getAttribute("label") || "Map marker";
800
+ this.textContent ||= "◆";
801
+ }
802
+ }
803
+
804
+ class GessiMap extends HTMLElementBase {
805
+ connectedCallback() {
806
+ if (this.dataset.enhanced) return;
807
+ this.dataset.enhanced = "true";
808
+ this.classList.add("gs-map");
809
+ this.dataset.effect = this.getAttribute("effect") || "none";
810
+ this.style.setProperty("--gs-map-fit", this.getAttribute("fit") || "contain");
811
+ this.style.setProperty("--gs-map-position", this.getAttribute("position") || "center");
812
+ const markers = [...this.querySelectorAll(":scope > gessi-marker")];
813
+ const viewport = document.createElement("div");
814
+ viewport.className = "gs-map-viewport";
815
+
816
+ if (this.hasAttribute("src")) {
817
+ const image = document.createElement("img");
818
+ image.src = this.getAttribute("src");
819
+ image.alt = this.getAttribute("alt") || "";
820
+ image.loading = "lazy";
821
+ image.decoding = "async";
822
+ applyMediaEffects(image, this.dataset.effect);
823
+ viewport.append(image);
824
+ } else {
825
+ const content = [...this.childNodes].filter((node) => !markers.includes(node));
826
+ viewport.append(...content);
827
+ }
828
+ viewport.append(...markers);
829
+ this.replaceChildren(viewport);
830
+ if (this.hasAttribute("caption")) {
831
+ const caption = document.createElement("p");
832
+ caption.className = "gs-map-caption";
833
+ caption.textContent = this.getAttribute("caption");
834
+ this.append(caption);
835
+ }
836
+ }
837
+ }
838
+
839
+ class GessiCarousel extends HTMLElementBase {
840
+ #index = 0;
841
+ #timer;
842
+
843
+ connectedCallback() {
844
+ if (this.dataset.enhanced) return;
845
+ this.dataset.enhanced = "true";
846
+ this.classList.add("gs-carousel");
847
+ const slides = [...this.children];
848
+ if (!slides.length) return;
849
+
850
+ const track = document.createElement("div");
851
+ track.className = "gs-carousel-track";
852
+ slides.forEach((slide, index) => {
853
+ slide.classList.add("gs-carousel-slide");
854
+ slide.dataset.slide = String(index);
855
+ track.append(slide);
856
+ });
857
+
858
+ const controls = document.createElement("nav");
859
+ controls.className = "gs-carousel-controls";
860
+ controls.ariaLabel = "Carousel controls";
861
+ const previous = makeButton("Previous slide", "◀", "previous");
862
+ const counter = document.createElement("output");
863
+ const next = makeButton("Next slide", "▶", "next");
864
+ controls.append(previous, counter, next);
865
+ this.replaceChildren(track, controls);
866
+
867
+ this.setAttribute("role", "region");
868
+ this.setAttribute("aria-roledescription", "carousel");
869
+ this.tabIndex ||= 0;
870
+ const show = (index) => {
871
+ this.#index = (index + slides.length) % slides.length;
872
+ slides.forEach((slide, slideIndex) => {
873
+ const active = slideIndex === this.#index;
874
+ slide.hidden = !active;
875
+ slide.setAttribute("aria-hidden", String(!active));
876
+ });
877
+ counter.value = `${this.#index + 1} / ${slides.length}`;
878
+ this.dispatchEvent(new CustomEvent("gs-slide-change", {
879
+ detail: { index: this.#index },
880
+ }));
881
+ };
882
+ previous.addEventListener("click", () => show(this.#index - 1));
883
+ next.addEventListener("click", () => show(this.#index + 1));
884
+ this.addEventListener("keydown", (event) => {
885
+ if (event.key === "ArrowLeft") show(this.#index - 1);
886
+ if (event.key === "ArrowRight") show(this.#index + 1);
887
+ if (event.key === "Home") show(0);
888
+ if (event.key === "End") show(slides.length - 1);
889
+ });
890
+ show(Number(this.getAttribute("start") || 0));
891
+
892
+ const autoplay = Number(this.getAttribute("autoplay") || 0);
893
+ if (autoplay > 0) {
894
+ this.#timer = setInterval(() => show(this.#index + 1), autoplay);
895
+ this.addEventListener("pointerenter", () => clearInterval(this.#timer));
896
+ this.addEventListener("focusin", () => clearInterval(this.#timer));
897
+ }
898
+ }
899
+
900
+ disconnectedCallback() {
901
+ clearInterval(this.#timer);
902
+ }
903
+ }
904
+
905
+ if (globalThis.customElements) {
906
+ if (!customElements.get("gessi-desktop")) customElements.define("gessi-desktop", GessiDesktop);
907
+ if (!customElements.get("gessi-window")) customElements.define("gessi-window", GessiWindow);
908
+ if (!customElements.get("gessi-dialog")) customElements.define("gessi-dialog", GessiDialog);
909
+ if (!customElements.get("gessi-icon")) customElements.define("gessi-icon", GessiIcon);
910
+ if (!customElements.get("gessi-icons")) customElements.define("gessi-icons", GessiIcons);
911
+ if (!customElements.get("gessi-tabs")) customElements.define("gessi-tabs", GessiTabs);
912
+ if (!customElements.get("gessi-panel")) customElements.define("gessi-panel", GessiPanel);
913
+ if (!customElements.get("gessi-meter")) customElements.define("gessi-meter", GessiMeter);
914
+ if (!customElements.get("gessi-list")) customElements.define("gessi-list", GessiList);
915
+ if (!customElements.get("gessi-alert")) customElements.define("gessi-alert", GessiAlert);
916
+ if (!customElements.get("gessi-toolbar")) customElements.define("gessi-toolbar", GessiToolbar);
917
+ if (!customElements.get("gessi-dock")) customElements.define("gessi-dock", GessiDock);
918
+ if (!customElements.get("gessi-menu")) customElements.define("gessi-menu", GessiMenu);
919
+ if (!customElements.get("gessi-breadcrumb")) customElements.define("gessi-breadcrumb", GessiBreadcrumb);
920
+ if (!customElements.get("gessi-tree")) customElements.define("gessi-tree", GessiTree);
921
+ if (!customElements.get("gessi-separator")) customElements.define("gessi-separator", GessiSeparator);
922
+ if (!customElements.get("gessi-tooltip")) customElements.define("gessi-tooltip", GessiTooltip);
923
+ if (!customElements.get("gessi-toast")) customElements.define("gessi-toast", GessiToast);
924
+ if (!customElements.get("gessi-media")) customElements.define("gessi-media", GessiMedia);
925
+ if (!customElements.get("gessi-map")) customElements.define("gessi-map", GessiMap);
926
+ if (!customElements.get("gessi-marker")) customElements.define("gessi-marker", GessiMarker);
927
+ if (!customElements.get("gessi-carousel")) customElements.define("gessi-carousel", GessiCarousel);
928
+ }
929
+
930
+ export {
931
+ GessiDesktop,
932
+ GessiDialog,
933
+ GessiIcon,
934
+ GessiIcons,
935
+ GessiMeter,
936
+ GessiPanel,
937
+ GessiTabs,
938
+ GessiWindow,
939
+ GessiList,
940
+ GessiAlert,
941
+ GessiToolbar,
942
+ GessiDock,
943
+ GessiMenu,
944
+ GessiBreadcrumb,
945
+ GessiTree,
946
+ GessiSeparator,
947
+ GessiTooltip,
948
+ GessiToast,
949
+ GessiMedia,
950
+ GessiMap,
951
+ GessiMarker,
952
+ GessiCarousel,
953
+ };