@@ -3005,6 +3005,158 @@ catch (e) {}
30053005 }
30063006 };
30073007
3008+ // ── Modal system ──────────────────────────────────────────────────
3009+ var modal = {
3010+ open: function(id) {
3011+ var el = document.querySelector('[data-stx-modal="' + id + '"]');
3012+ if (!el) { console.warn('[stx:modal] Modal "' + id + '" not found'); return; }
3013+ el.style.display = 'flex';
3014+ // Force reflow then animate
3015+ void el.offsetHeight;
3016+ el.setAttribute('data-stx-modal-open', '');
3017+ document.body.style.overflow = 'hidden';
3018+ // Escape key handler
3019+ if (el.getAttribute('data-close-escape') !== 'false') {
3020+ var escHandler = function(e) {
3021+ if (e.key === 'Escape') { modal.close(id); document.removeEventListener('keydown', escHandler); }
3022+ };
3023+ document.addEventListener('keydown', escHandler);
3024+ el._stxEscHandler = escHandler;
3025+ }
3026+ // Backdrop click
3027+ if (el.getAttribute('data-close-backdrop') !== 'false') {
3028+ el.onclick = function(e) { if (e.target === el) modal.close(id); };
3029+ }
3030+ },
3031+ close: function(id) {
3032+ var el = document.querySelector('[data-stx-modal="' + id + '"]');
3033+ if (!el) return;
3034+ el.removeAttribute('data-stx-modal-open');
3035+ if (el._stxEscHandler) { document.removeEventListener('keydown', el._stxEscHandler); el._stxEscHandler = null; }
3036+ el.onclick = null;
3037+ setTimeout(function() {
3038+ el.style.display = 'none';
3039+ // Restore scroll if no other modals are open
3040+ if (!document.querySelector('[data-stx-modal-open]')) document.body.style.overflow = '';
3041+ }, 200);
3042+ },
3043+ toggle: function(id) {
3044+ var el = document.querySelector('[data-stx-modal="' + id + '"]');
3045+ if (el && el.hasAttribute('data-stx-modal-open')) modal.close(id);
3046+ else modal.open(id);
3047+ }
3048+ };
3049+
3050+ // ── Alert & Confirm dialogs ─────────────────────────────────────
3051+ // Styled replacements for window.alert() and window.confirm().
3052+ // Both return Promises and render into a temporary modal overlay.
3053+
3054+ var _dialogId = 0;
3055+ var _dialogIcons = {
3056+ info: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
3057+ warning: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#d97706" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
3058+ error: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
3059+ success: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9 12l2 2 4-4"/></svg>',
3060+ question: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
3061+ };
3062+
3063+ function _createDialog(message, options, isConfirm) {
3064+ var opts = options || {};
3065+ var type = opts.type || (isConfirm ? 'question' : 'info');
3066+ var title = opts.title || '';
3067+ var confirmText = opts.confirmText || 'OK';
3068+ var cancelText = opts.cancelText || 'Cancel';
3069+ var id = 'stx-dialog-' + (++_dialogId);
3070+ var isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
3071+ var bg = isDark ? '#1f2937' : '#ffffff';
3072+ var textColor = isDark ? '#f3f4f6' : '#1f2937';
3073+ var subColor = isDark ? '#9ca3af' : '#6b7280';
3074+ var icon = _dialogIcons[type] || _dialogIcons.info;
3075+
3076+ return new Promise(function(resolve) {
3077+ var backdrop = document.createElement('div');
3078+ backdrop.id = id;
3079+ backdrop.style.cssText = 'position:fixed;inset:0;z-index:999999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);backdrop-filter:blur(2px);opacity:0;transition:opacity 0.2s ease;font-family:system-ui,-apple-system,sans-serif';
3080+ backdrop.setAttribute('role', 'alertdialog');
3081+ backdrop.setAttribute('aria-modal', 'true');
3082+
3083+ var panel = document.createElement('div');
3084+ panel.style.cssText = 'max-width:24rem;width:calc(100% - 2rem);border-radius:0.75rem;padding:1.5rem;background:' + bg + ';color:' + textColor + ';box-shadow:0 20px 60px rgba(0,0,0,0.3);transform:scale(0.95);transition:transform 0.2s ease;text-align:center';
3085+
3086+ var iconDiv = document.createElement('div');
3087+ iconDiv.style.cssText = 'display:flex;justify-content:center;margin-bottom:1rem';
3088+ iconDiv.innerHTML = icon;
3089+ panel.appendChild(iconDiv);
3090+
3091+ if (title) {
3092+ var titleEl = document.createElement('h3');
3093+ titleEl.style.cssText = 'margin:0 0 0.5rem;font-size:1.125rem;font-weight:600';
3094+ titleEl.textContent = title;
3095+ panel.appendChild(titleEl);
3096+ }
3097+
3098+ var msgEl = document.createElement('p');
3099+ msgEl.style.cssText = 'margin:0 0 1.25rem;font-size:0.875rem;line-height:1.5;color:' + subColor;
3100+ msgEl.textContent = message;
3101+ panel.appendChild(msgEl);
3102+
3103+ var btnRow = document.createElement('div');
3104+ btnRow.style.cssText = 'display:flex;gap:0.75rem;justify-content:center';
3105+
3106+ function cleanup(result) {
3107+ backdrop.style.opacity = '0';
3108+ panel.style.transform = 'scale(0.95)';
3109+ setTimeout(function() { backdrop.remove(); }, 200);
3110+ resolve(result);
3111+ }
3112+
3113+ if (isConfirm) {
3114+ var cancelBtn = document.createElement('button');
3115+ cancelBtn.textContent = cancelText;
3116+ cancelBtn.style.cssText = 'padding:0.5rem 1.25rem;border-radius:0.5rem;font-size:0.875rem;font-weight:500;cursor:pointer;border:1px solid ' + (isDark ? '#374151' : '#d1d5db') + ';background:transparent;color:' + textColor + ';transition:background 0.15s';
3117+ cancelBtn.onmouseover = function() { this.style.background = isDark ? '#374151' : '#f3f4f6'; };
3118+ cancelBtn.onmouseout = function() { this.style.background = 'transparent'; };
3119+ cancelBtn.onclick = function() { cleanup(false); };
3120+ btnRow.appendChild(cancelBtn);
3121+ }
3122+
3123+ var okBtn = document.createElement('button');
3124+ okBtn.textContent = confirmText;
3125+ var btnColor = type === 'error' ? '#dc2626' : type === 'warning' ? '#d97706' : '#5672cd';
3126+ okBtn.style.cssText = 'padding:0.5rem 1.25rem;border-radius:0.5rem;font-size:0.875rem;font-weight:500;cursor:pointer;border:none;background:' + btnColor + ';color:#fff;transition:opacity 0.15s';
3127+ okBtn.onmouseover = function() { this.style.opacity = '0.9'; };
3128+ okBtn.onmouseout = function() { this.style.opacity = '1'; };
3129+ okBtn.onclick = function() { cleanup(isConfirm ? true : undefined); };
3130+ btnRow.appendChild(okBtn);
3131+
3132+ panel.appendChild(btnRow);
3133+ backdrop.appendChild(panel);
3134+ document.body.appendChild(backdrop);
3135+
3136+ // Animate in
3137+ void backdrop.offsetHeight;
3138+ backdrop.style.opacity = '1';
3139+ panel.style.transform = 'scale(1)';
3140+
3141+ // Escape key
3142+ var escHandler = function(e) {
3143+ if (e.key === 'Escape') { cleanup(isConfirm ? false : undefined); document.removeEventListener('keydown', escHandler); }
3144+ };
3145+ document.addEventListener('keydown', escHandler);
3146+
3147+ // Focus the primary button
3148+ okBtn.focus();
3149+ });
3150+ }
3151+
3152+ function stxAlert(message, options) {
3153+ return _createDialog(message, options, false);
3154+ }
3155+
3156+ function stxConfirm(message, options) {
3157+ return _createDialog(message, options, true);
3158+ }
3159+
30083160 // Component mount system
30093161 var mountQueue = [];
30103162
@@ -3054,6 +3206,9 @@ catch (e) {}
30543206 useHead,
30553207 useSeoMeta,
30563208 toast,
3209+ modal,
3210+ alert: stxAlert,
3211+ confirm: stxConfirm,
30573212 helpers: globalHelpers,
30583213
30593214 // Component composition API (Phase 4)
@@ -3510,6 +3665,9 @@ else {
35103665 window.useHead = useHead;
35113666 window.useSeoMeta = useSeoMeta;
35123667 window.toast = toast;
3668+ window.modal = modal;
3669+ window.stxAlert = stxAlert;
3670+ window.stxConfirm = stxConfirm;
35133671 window.defineStore = window.stx.defineStore;
35143672 window.useStore = window.stx.useStore;
35153673 window.ref = state;
0 commit comments