@rxxuzi/gumi
Version:
Clean & minimal design system with delightful interactions
1,301 lines (1,292 loc) • 104 kB
JavaScript
/*!
* Gumi.js v1.0.0
* Clean & minimal design system with delightful interactions 🍬
* https://github.com/rxxuzi/gumi
* (c) 2025 rxxuzi
* Released under the MIT License
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.gumi = factory());
})(this, (function () { 'use strict';
// core/dom.ts
// Gumi.js v1.0.0 - DOM Utilities
/**
* Query selector helper
*/
function $(selector) {
if (typeof selector === 'string') {
return document.querySelector(selector);
}
return selector;
}
/**
* Query selector all helper
*/
function $$(selector) {
return document.querySelectorAll(selector);
}
/**
* Ready function - fires when DOM is ready
*/
function ready(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn);
}
else {
fn();
}
}
function createElement(tag, options = {}) {
const el = document.createElement(tag);
if (options.className)
el.className = options.className;
if (options.id)
el.id = options.id;
if (options.text)
el.textContent = options.text;
if (options.html)
el.innerHTML = options.html;
if (options.attributes) {
Object.entries(options.attributes).forEach(([key, value]) => {
el.setAttribute(key, value);
});
}
if (options.style) {
Object.assign(el.style, options.style);
}
if (options.events) {
Object.entries(options.events).forEach(([event, handler]) => {
el.addEventListener(event, handler);
});
}
if (options.children) {
options.children.forEach(child => {
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child));
}
else {
el.appendChild(child);
}
});
}
return el;
}
/**
* Add event listener with delegation
*/
function on(element, event, selectorOrHandler, handler) {
const el = element instanceof Document || element instanceof Window
? element
: $(element);
if (!el)
return;
if (typeof selectorOrHandler === 'function') {
el.addEventListener(event, selectorOrHandler);
}
else {
el.addEventListener(event, (e) => {
const target = e.target;
const delegateTarget = target.closest(selectorOrHandler);
if (delegateTarget && handler) {
handler.call(delegateTarget, e);
}
});
}
}
/**
* Remove event listener
*/
function off(element, event, handler) {
const el = element instanceof Document || element instanceof Window
? element
: $(element);
if (!el)
return;
el.removeEventListener(event, handler);
}
/**
* Trigger custom event
*/
function trigger(element, eventName, detail) {
const el = $(element);
if (!el)
return;
const event = new CustomEvent(eventName, {
bubbles: true,
cancelable: true,
detail
});
el.dispatchEvent(event);
}
/**
* Check if element has class
*/
function hasClass(element, className) {
const el = $(element);
return el ? el.classList.contains(className) : false;
}
/**
* Add class(es) to element
*/
function addClass(element, ...classNames) {
const el = $(element);
if (!el)
return;
el.classList.add(...classNames);
}
/**
* Remove class(es) from element
*/
function removeClass(element, ...classNames) {
const el = $(element);
if (!el)
return;
el.classList.remove(...classNames);
}
/**
* Show element
*/
function show(element) {
const el = $(element);
if (!el)
return;
const display = el.style.display;
if (display === 'none') {
el.style.display = '';
}
if (window.getComputedStyle(el).display === 'none') {
el.style.display = 'block';
}
}
/**
* Hide element
*/
function hide(element) {
const el = $(element);
if (!el)
return;
el.style.display = 'none';
}
/**
* Toggle element visibility
*/
function toggle(element) {
const el = $(element);
if (!el)
return;
if (window.getComputedStyle(el).display === 'none') {
show(el);
}
else {
hide(el);
}
}
// core/animation.ts
// Gumi.js v1.0.0 - Animation Utilities
/**
* Animate element using Web Animations API
*/
function animate(element, keyframes, options = {}) {
const el = $(element);
if (!el)
return Promise.resolve();
const defaultOptions = {
duration: options.duration || 300,
easing: options.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
fill: options.fill || 'forwards',
delay: options.delay || 0,
iterations: options.iterations || 1,
direction: options.direction || 'normal'
};
const animation = el.animate(keyframes, defaultOptions);
return new Promise((resolve) => {
animation.onfinish = () => resolve();
animation.oncancel = () => resolve();
});
}
/**
* Fade in animation
*/
function fadeIn(element, options = {}) {
const el = $(element);
if (!el)
return Promise.resolve();
el.style.opacity = '0';
el.style.visibility = 'visible';
return animate(el, [
{ opacity: 0 },
{ opacity: 1 }
], {
duration: options.duration || 600,
...options
});
}
/**
* Fade out animation
*/
function fadeOut(element, options = {}) {
const el = $(element);
if (!el)
return Promise.resolve();
return animate(el, [
{ opacity: 1 },
{ opacity: 0 }
], {
duration: options.duration || 300,
...options
}).then(() => {
if (options.fill !== 'none') {
el.style.visibility = 'hidden';
}
});
}
/**
* Slide up animation (hide)
*/
function slideUp(element, options = {}) {
const el = $(element);
if (!el)
return Promise.resolve();
const height = el.offsetHeight;
el.style.overflow = 'hidden';
return animate(el, [
{ height: `${height}px`, opacity: 1 },
{ height: '0px', opacity: 0 }
], {
duration: options.duration || 300,
...options
}).then(() => {
el.style.display = 'none';
el.style.height = '';
el.style.overflow = '';
});
}
/**
* Slide down animation (show)
*/
function slideDown(element, options = {}) {
const el = $(element);
if (!el)
return Promise.resolve();
// Get the height
el.style.display = 'block';
el.style.height = 'auto';
const height = el.offsetHeight;
el.style.height = '0px';
el.style.overflow = 'hidden';
return animate(el, [
{ height: '0px', opacity: 0 },
{ height: `${height}px`, opacity: 1 }
], {
duration: options.duration || 300,
...options
}).then(() => {
el.style.height = '';
el.style.overflow = '';
});
}
/**
* Scale in animation
*/
function scaleIn(element, options = {}) {
const el = $(element);
if (!el)
return Promise.resolve();
el.style.visibility = 'visible';
return animate(el, [
{ transform: 'scale(0.9)', opacity: 0 },
{ transform: 'scale(1)', opacity: 1 }
], {
duration: options.duration || 300,
...options
});
}
/**
* Scale out animation
*/
function scaleOut(element, options = {}) {
const el = $(element);
if (!el)
return Promise.resolve();
return animate(el, [
{ transform: 'scale(1)', opacity: 1 },
{ transform: 'scale(0.9)', opacity: 0 }
], {
duration: options.duration || 300,
...options
});
}
/**
* Slide in from direction
*/
function slideIn(element, direction = 'left', options = {}) {
const el = $(element);
if (!el)
return Promise.resolve();
const transforms = {
left: 'translateX(-100%)',
right: 'translateX(100%)',
top: 'translateY(-100%)',
bottom: 'translateY(100%)'
};
el.style.visibility = 'visible';
return animate(el, [
{ transform: transforms[direction], opacity: 0 },
{ transform: 'translate(0)', opacity: 1 }
], {
duration: options.duration || 300,
...options
});
}
/**
* Bounce animation
*/
function bounce(element, options = {}) {
const el = $(element);
if (!el)
return Promise.resolve();
return animate(el, [
{ transform: 'translateY(0)' },
{ transform: 'translateY(-20px)' },
{ transform: 'translateY(0)' }
], {
duration: options.duration || 1000,
...options
});
}
/**
* Shake animation
*/
function shake(element, options = {}) {
const el = $(element);
if (!el)
return Promise.resolve();
return animate(el, [
{ transform: 'translateX(0)' },
{ transform: 'translateX(-10px)' },
{ transform: 'translateX(10px)' },
{ transform: 'translateX(-10px)' },
{ transform: 'translateX(10px)' },
{ transform: 'translateX(0)' }
], {
duration: options.duration || 500,
...options
});
}
/**
* Pulse animation
*/
function pulse(element, options = {}) {
const el = $(element);
if (!el)
return Promise.resolve();
return animate(el, [
{ transform: 'scale(1)', opacity: 1 },
{ transform: 'scale(1.05)', opacity: 0.8 },
{ transform: 'scale(1)', opacity: 1 }
], {
duration: options.duration || 2000,
...options
});
}
/**
* Ripple effect
*/
function ripple(event, element) {
const el = element ? $(element) : event.currentTarget;
if (!el)
return;
const ripple = document.createElement('span');
const rect = el.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
top: ${y}px;
left: ${x}px;
pointer-events: none;
transform: scale(0);
`;
el.style.position = 'relative';
el.style.overflow = 'hidden';
el.appendChild(ripple);
animate(ripple, [
{ transform: 'scale(0)', opacity: 1 },
{ transform: 'scale(4)', opacity: 0 }
], { duration: 600 }).then(() => {
ripple.remove();
});
}
// components/theme.ts
// Gumi.js v1.0.0 - Theme Management
class ThemeManager {
constructor() {
this.currentTheme = 'light';
this.storageKey = 'apple-theme';
this.init();
}
/**
* Initialize theme
*/
init() {
// Check for saved theme or system preference
const savedTheme = localStorage.getItem(this.storageKey);
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme) {
this.setTheme(savedTheme);
}
else if (systemPrefersDark) {
this.setTheme('dark');
}
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem(this.storageKey)) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
}
/**
* Get current theme
*/
getTheme() {
return this.currentTheme;
}
/**
* Set theme
*/
setTheme(theme) {
if (theme === 'auto') {
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
theme = systemPrefersDark ? 'dark' : 'light';
}
this.currentTheme = theme;
document.documentElement.setAttribute('data-theme', theme);
// Save preference
localStorage.setItem(this.storageKey, theme);
// Dispatch theme change event
trigger(document.documentElement, 'apple-theme-change', { theme });
}
/**
* Toggle theme
*/
toggleTheme() {
const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
this.setTheme(newTheme);
}
/**
* Clear theme preference
*/
clearPreference() {
localStorage.removeItem(this.storageKey);
this.init();
}
/**
* Create theme toggle button
*/
createToggleButton(options = {}) {
const button = document.createElement('button');
button.className = options.className || 'btn-icon btn-ghost';
button.setAttribute('aria-label', 'Toggle theme');
const updateIcon = () => {
const isDark = this.currentTheme === 'dark';
button.innerHTML = isDark
? (options.darkIcon || '<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>')
: (options.lightIcon || '<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>');
};
updateIcon();
button.addEventListener('click', () => {
this.toggleTheme();
updateIcon();
});
// Listen for theme changes from other sources
window.addEventListener('apple-theme-change', updateIcon);
return button;
}
}
// utils/helpers.ts
// Gumi.js v1.0.0 - Helper Utilities
/**
* Debounce function
*/
function debounce(func, wait) {
let timeout = null;
return function executedFunction(...args) {
const later = () => {
timeout = null;
func(...args);
};
if (timeout)
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Throttle function
*/
function throttle(func, limit) {
let inThrottle = false;
return function executedFunction(...args) {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
/**
* Generate unique ID
*/
function generateId(prefix = 'apple') {
return `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Clamp number between min and max
*/
function clamp(num, min, max) {
return Math.max(min, Math.min(max, num));
}
/**
* Copy text to clipboard
*/
async function copyToClipboard(text) {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
}
else {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
textArea.remove();
return successful;
}
}
catch (err) {
console.error('Failed to copy text:', err);
return false;
}
}
// components/modal.ts
// Gumi.js v1.0.0 - Modal Component
class Modal {
constructor(element, options = {}) {
this.backdrop = null;
this.isOpen = false;
this.escapeHandler = null;
this.keydownListener = null;
const el = $(element);
if (!el)
throw new Error('Modal element not found');
this.element = el;
this.options = {
backdrop: true,
keyboard: true,
focus: true,
...options
};
this.init();
}
/**
* Initialize modal
*/
init() {
// Set initial styles
this.element.style.display = 'none';
this.element.setAttribute('role', 'dialog');
this.element.setAttribute('aria-modal', 'true');
if (!this.element.id) {
this.element.id = generateId('modal');
}
}
/**
* Open modal
*/
open() {
if (this.isOpen)
return;
this.isOpen = true;
// Create backdrop if needed
if (this.options.backdrop) {
this.createBackdrop();
}
// Style the modal
Object.assign(this.element.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%) scale(0.9)',
zIndex: '1050',
opacity: '0',
maxWidth: '90vw',
maxHeight: '90vh',
overflow: 'auto',
display: 'block'
});
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Animate in
if (this.backdrop) {
animate(this.backdrop, [
{ opacity: 0 },
{ opacity: 1 }
], { duration: 200 });
}
animate(this.element, [
{ opacity: 0, transform: 'translate(-50%, -50%) scale(0.9)' },
{ opacity: 1, transform: 'translate(-50%, -50%) scale(1)' }
], { duration: 300 });
// Focus management
if (this.options.focus) {
this.element.focus();
}
// Keyboard support
if (this.options.keyboard) {
this.escapeHandler = (e) => {
if (e.key === 'Escape') {
this.close();
}
};
this.keydownListener = (e) => {
if (this.escapeHandler) {
this.escapeHandler(e);
}
};
on(document, 'keydown', this.keydownListener);
}
// Dispatch open event
trigger(this.element, 'modal-open', { modal: this.element });
}
/**
* Close modal
*/
close() {
if (!this.isOpen)
return;
this.isOpen = false;
// Remove escape handler
if (this.keydownListener) {
off(document, 'keydown', this.keydownListener);
this.keydownListener = null;
this.escapeHandler = null;
}
// Animate out
const animations = [
animate(this.element, [
{ opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
{ opacity: 0, transform: 'translate(-50%, -50%) scale(0.9)' }
], { duration: 200 })
];
if (this.backdrop) {
animations.push(animate(this.backdrop, [
{ opacity: 1 },
{ opacity: 0 }
], { duration: 200 }));
}
Promise.all(animations).then(() => {
this.element.style.display = 'none';
document.body.style.overflow = '';
if (this.backdrop) {
this.backdrop.remove();
this.backdrop = null;
}
// Dispatch close event
trigger(this.element, 'modal-close', { modal: this.element });
});
}
/**
* Toggle modal
*/
toggle() {
if (this.isOpen) {
this.close();
}
else {
this.open();
}
}
/**
* Create backdrop
*/
createBackdrop() {
// Remove any existing backdrop
const existingBackdrop = $('.gumi-modal-backdrop');
if (existingBackdrop) {
existingBackdrop.remove();
}
this.backdrop = document.createElement('div');
this.backdrop.className = 'gumi-modal-backdrop';
Object.assign(this.backdrop.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
background: 'rgba(0, 0, 0, 0.5)',
backdropFilter: 'blur(4px)',
zIndex: '1040',
opacity: '0'
});
// Close on backdrop click
on(this.backdrop, 'click', () => this.close());
document.body.appendChild(this.backdrop);
}
/**
* Destroy modal instance
*/
destroy() {
this.close();
this.element.removeAttribute('role');
this.element.removeAttribute('aria-modal');
}
/**
* Static method to initialize modals from triggers
*/
static initFromTriggers(selector = '[data-modal]') {
const triggers = $$(selector);
const modals = [];
triggers.forEach(trigger => {
const modalId = trigger.getAttribute('data-modal');
if (!modalId)
return;
const modalEl = $(modalId);
if (!modalEl)
return;
const modal = new Modal(modalEl);
modals.push(modal);
on(trigger, 'click', (e) => {
e.preventDefault();
modal.open();
});
});
return modals;
}
}
// toast.ts
// Beautiful toast notifications
class Toast {
/**
* Show a toast notification
*/
static show(message, options = {}) {
const config = {
type: 'info',
duration: 4000,
position: 'top-right',
...options
};
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// Create container if needed
this.createContainer(config.position);
// Create toast element
const toast = createElement('div', {
className: `gumi-toast ${config.type} entering`,
html: `
${this.getIcon(config.type)}
<span>${message}</span>
`
});
// Store reference
this.toasts.set(id, toast);
// Add to container
this.container.appendChild(toast);
// Auto-remove after duration
setTimeout(() => {
this.remove(id);
}, config.duration);
// Click to dismiss
on(toast, 'click', () => {
this.remove(id);
});
// Trigger custom event
trigger(document.body, 'gumi-toast-show', {
id,
message,
type: config.type
});
return id;
}
/**
* Remove a specific toast
*/
static remove(id) {
const toast = this.toasts.get(id);
if (!toast)
return;
removeClass(toast, 'entering');
addClass(toast, 'exiting');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
this.toasts.delete(id);
}, 200);
trigger(document.body, 'gumi-toast-hide', { id });
}
/**
* Remove all toasts
*/
static removeAll() {
Array.from(this.toasts.keys()).forEach(id => this.remove(id));
}
/**
* Create container for toasts
*/
static createContainer(position) {
if (this.container)
return;
this.container = createElement('div', {
className: `gumi-toast-container ${position}`
});
document.body.appendChild(this.container);
}
/**
* Get icon for toast type
*/
static getIcon(type) {
const icons = {
success: `<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 12l2 2 4-4"></path>
<circle cx="12" cy="12" r="9"></circle>
</svg>`,
error: `<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="9"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>`,
warning: `<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>`,
info: `<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="9"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>`
};
return icons[type] || icons.info;
}
/**
* Helper methods for different toast types
*/
static success(message, options = {}) {
return this.show(message, { ...options, type: 'success' });
}
static error(message, options = {}) {
return this.show(message, { ...options, type: 'error' });
}
static warning(message, options = {}) {
return this.show(message, { ...options, type: 'warning' });
}
static info(message, options = {}) {
return this.show(message, { ...options, type: 'info' });
}
}
Toast.container = null;
Toast.toasts = new Map();
// components/tabs.ts
// Gumi.js v1.0.0 - Tabs Component
class Tabs {
constructor(container, options = {}) {
this.tabs = [];
this.panels = [];
this.activeIndex = 0;
const el = $(container);
if (!el)
throw new Error('Tabs container not found');
this.container = el;
this.options = {
activeIndex: 0,
...options
};
this.init();
}
/**
* Initialize tabs
*/
init() {
// Find tabs and panels - support both old and new structure
const tabList = this.container.querySelector('.tab-list');
if (tabList) {
// New structure: .tabs > .tab-list > .tab-button
this.tabs = Array.from(tabList.querySelectorAll('.tab-button'));
}
else {
// Legacy structure: .tabs > .tab
this.tabs = Array.from(this.container.querySelectorAll('.tab'));
}
// Find panels by data-tab attribute
this.tabs.forEach((tab, index) => {
const panelId = tab.getAttribute('data-tab');
if (panelId) {
const panel = $(panelId);
if (panel) {
this.panels[index] = panel;
panel.setAttribute('role', 'tabpanel');
panel.setAttribute('aria-labelledby', tab.id || `tab-${index}`);
panel.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
}
}
// Set ARIA attributes
tab.setAttribute('role', 'tab');
tab.setAttribute('aria-selected', 'false');
tab.setAttribute('tabindex', '-1');
if (!tab.id)
tab.id = `tab-${index}`;
// Add click handler
on(tab, 'click', () => this.selectTab(index));
});
// Set container ARIA attributes
if (tabList) {
tabList.setAttribute('role', 'tablist');
}
else {
this.container.setAttribute('role', 'tablist');
}
// Activate initial tab
if (this.options.activeIndex !== undefined) {
this.selectTab(this.options.activeIndex);
}
else if (this.tabs.length > 0) {
// Check for active class
const activeTab = this.tabs.findIndex(tab => tab.classList.contains('active'));
this.selectTab(activeTab >= 0 ? activeTab : 0);
}
}
/**
* Select tab by index with smooth animation
*/
async selectTab(index) {
if (index < 0 || index >= this.tabs.length || index === this.activeIndex)
return;
const previousPanel = this.panels[this.activeIndex];
const selectedTab = this.tabs[index];
const selectedPanel = this.panels[index];
// Update tab states immediately for visual feedback
this.tabs.forEach((tab, i) => {
removeClass(tab, 'active');
tab.setAttribute('aria-selected', 'false');
tab.setAttribute('tabindex', '-1');
});
addClass(selectedTab, 'active');
selectedTab.setAttribute('aria-selected', 'true');
selectedTab.setAttribute('tabindex', '0');
// If we have panels to animate
if (previousPanel && selectedPanel && previousPanel !== selectedPanel) {
// Add transition classes
addClass(previousPanel, 'tab-panel-exit');
addClass(selectedPanel, 'tab-panel-enter');
// Set up the new panel
selectedPanel.style.display = 'block';
selectedPanel.style.opacity = '0';
selectedPanel.style.transform = 'translateX(10px)';
// Start animations
previousPanel.style.opacity = '0';
previousPanel.style.transform = 'translateX(-10px)';
// Wait for exit animation
await new Promise(resolve => setTimeout(resolve, 150));
// Hide previous panel
previousPanel.style.display = 'none';
previousPanel.style.opacity = '';
previousPanel.style.transform = '';
removeClass(previousPanel, 'tab-panel-exit');
// Animate in new panel
selectedPanel.style.opacity = '1';
selectedPanel.style.transform = 'translateX(0)';
// Clean up after animation
setTimeout(() => {
removeClass(selectedPanel, 'tab-panel-enter');
selectedPanel.style.opacity = '';
selectedPanel.style.transform = '';
}, 200);
}
else if (selectedPanel) {
// Simple display for first load
this.panels.forEach((panel, i) => {
if (panel) {
panel.style.display = i === index ? 'block' : 'none';
}
});
}
this.activeIndex = index;
// Call onChange callback
if (this.options.onChange) {
this.options.onChange(index);
}
// Dispatch event
trigger(this.container, 'tab-change', { index });
}
/**
* Get active tab index
*/
getActiveIndex() {
return this.activeIndex;
}
/**
* Next tab
*/
next() {
const nextIndex = (this.activeIndex + 1) % this.tabs.length;
this.selectTab(nextIndex);
}
/**
* Previous tab
*/
previous() {
const prevIndex = this.activeIndex === 0 ? this.tabs.length - 1 : this.activeIndex - 1;
this.selectTab(prevIndex);
}
/**
* Add keyboard navigation
*/
enableKeyboardNavigation() {
on(this.container, 'keydown', (e) => {
const event = e;
switch (event.key) {
case 'ArrowLeft':
event.preventDefault();
this.previous();
this.tabs[this.activeIndex].focus();
break;
case 'ArrowRight':
event.preventDefault();
this.next();
this.tabs[this.activeIndex].focus();
break;
case 'Home':
event.preventDefault();
this.selectTab(0);
this.tabs[0].focus();
break;
case 'End':
event.preventDefault();
this.selectTab(this.tabs.length - 1);
this.tabs[this.tabs.length - 1].focus();
break;
}
});
}
/**
* Destroy tabs instance
*/
destroy() {
this.container.removeAttribute('role');
this.tabs.forEach(tab => {
tab.removeAttribute('role');
tab.removeAttribute('aria-selected');
});
this.panels.forEach(panel => {
panel.removeAttribute('role');
panel.removeAttribute('aria-labelledby');
});
}
/**
* Static method to initialize all tabs
*/
static initAll(selector = '.tabs') {
const containers = $$(selector);
return Array.from(containers).map(container => new Tabs(container));
}
}
// utils/icons.ts
// Gumi.js v1.0.0 - SVG Icons
const icons = {
check: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>',
x: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',
info: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>',
warning: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>',
error: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>',
success: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>',
chevronDown: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>',
chevronUp: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>',
chevronLeft: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>',
chevronRight: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>',
moon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>',
sun: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>',
loader: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line></svg>',
menu: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>',
search: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg>',
plus: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>',
minus: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>',
copy: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
heart: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>',
star: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>'
};
// components/accordion.ts
// Gumi.js v1.0.0 - Accordion Component
class Accordion {
constructor(container, options = {}) {
this.items = [];
const el = $(container);
if (!el)
throw new Error('Accordion container not found');
this.container = el;
this.options = {
multiple: false,
collapsed: true,
...options
};
this.init();
}
/**
* Initialize accordion
*/
init() {
// Set data attribute for multiple
if (this.options.multiple) {
this.container.setAttribute('data-multiple', 'true');
}
// Find all accordion items
this.items = Array.from(this.container.querySelectorAll('.accordion-item'));
this.items.forEach((item, index) => {
const header = item.querySelector('.accordion-header');
const content = item.querySelector('.accordion-content');
if (!header || !content)
return;
// Set ARIA attributes
header.setAttribute('role', 'button');
header.setAttribute('aria-expanded', 'false');
if (!header.id)
header.id = `accordion-header-${index}`;
content.setAttribute('role', 'region');
content.setAttribute('aria-labelledby', header.id);
// Add chevron icon if not exists
if (!header.querySelector('.accordion-icon')) {
const iconSpan = document.createElement('span');
iconSpan.className = 'accordion-icon';
iconSpan.innerHTML = icons.chevronDown;
header.appendChild(iconSpan);
}
// Set initial state
if (this.options.collapsed && !hasClass(item, 'active')) {
removeClass(item, 'active');
header.setAttribute('aria-expanded', 'false');
}
else if (hasClass(item, 'active')) {
header.setAttribute('aria-expanded', 'true');
}
// Add click handler
on(header, 'click', () => this.toggle(index));
});
}
/**
* Toggle accordion item
*/
toggle(index) {
const item = this.items[index];
if (!item)
return;
const isActive = hasClass(item, 'active');
if (isActive) {
this.close(index);
}
else {
this.open(index);
}
}
/**
* Open accordion item
*/
open(index) {
const item = this.items[index];
if (!item)
return;
const header = item.querySelector('.accordion-header');
const content = item.querySelector('.accordion-content');
if (!header || !content)
return;
// Close other items if not multiple
if (!this.options.multiple) {
this.items.forEach((otherItem, otherIndex) => {
if (otherIndex !== index && hasClass(otherItem, 'active')) {
this.close(otherIndex);
}
});
}
// Open current item
addClass(item, 'active');
addClass(header, 'active');
header.setAttribute('aria-expanded', 'true');
// Dispatch event
trigger(item, 'accordion-toggle', { item, open: true });
}
/**
* Close accordion item
*/
close(index) {
const item = this.items[index];
if (!item)
return;
const header = item.querySelector('.accordion-header');
const content = item.querySelector('.accordion-content');
if (!header || !content)
return;
removeClass(item, 'active');
removeClass(header, 'active');
header.setAttribute('aria-expanded', 'false');
// Dispatch event
trigger(item, 'accordion-toggle', { item, open: false });
}
/**
* Open all items
*/
openAll() {
this.items.forEach((_, index) => this.open(index));
}
/**
* Close all items
*/
closeAll() {
this.items.forEach((_, index) => this.close(index));
}
/**
* Get active items
*/
getActiveItems() {
return this.items
.map((item, index) => hasClass(item, 'active') ? index : -1)
.filter(index => index !== -1);
}
/**
* Enable keyboard navigation
*/
enableKeyboardNavigation() {
this.items.forEach((item, index) => {
const header = item.querySelector('.accordion-header');
if (!header)
return;
header.setAttribute('tabindex', '0');
on(header, 'keydown', (e) => {
const event = e;
switch (event.key) {
case 'Enter':
case ' ':
event.preventDefault();
this.toggle(index);
break;
case 'ArrowDown':
event.preventDefault();
const nextIndex = (index + 1) % this.items.length;
const nextHeader = this.items[nextIndex].querySelector('.accordion-header');
nextHeader === null || nextHeader === void 0 ? void 0 : nextHeader.focus();
break;
case 'ArrowUp':
event.preventDefault();
const prevIndex = index === 0 ? this.items.length - 1 : index