Revamp UI chrome and add notification sound alerts
- Add desktop-style title bar shell and viewport locking for bank/org UIs - Redesign notification HUD visuals and behavior (timers, persistence, exposed JS API) - Register and play a new notification sound via `CfgSounds` on client events
This commit is contained in:
parent
57267b79b6
commit
6eb6ac79d1
@ -4,6 +4,9 @@
|
||||
--bg-surface-hover: #f1f5f9;
|
||||
--primary: #475569;
|
||||
--primary-hover: #1e293b;
|
||||
--window-blue: #12325b;
|
||||
--window-blue-border: #214978;
|
||||
--window-blue-highlight: #d7e5f8;
|
||||
--text-main: #1f2937;
|
||||
--text-muted: #64748b;
|
||||
--text-inverse: #f8fafc;
|
||||
@ -13,6 +16,11 @@
|
||||
--footer-bg: #1e293b;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
margin: 0;
|
||||
@ -20,16 +28,107 @@ body {
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
line-height: 1.6;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.window-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.8rem 1.25rem;
|
||||
background: linear-gradient(180deg, var(--window-blue) 0%, #0d2643 100%);
|
||||
border-bottom: 1px solid var(--window-blue-border);
|
||||
color: var(--text-inverse);
|
||||
box-shadow: 0 10px 24px rgb(18 50 91 / 0.24);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.window-titlebar-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.window-titlebar-kicker {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: rgb(215 229 248 / 0.78);
|
||||
}
|
||||
|
||||
.window-titlebar-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.window-titlebar-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.window-control-btn {
|
||||
min-width: 2.5rem;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgb(215 229 248 / 0.22);
|
||||
background: rgb(255 255 255 / 0.08);
|
||||
color: var(--window-blue-highlight);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.window-control-btn:hover {
|
||||
background: rgb(255 255 255 / 0.08);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.window-control-btn:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.window-control-btn.is-close {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
border-color: rgb(255 255 255 / 0.24);
|
||||
}
|
||||
|
||||
.window-control-btn.is-close:hover {
|
||||
background: rgb(255 255 255 / 0.18);
|
||||
}
|
||||
|
||||
.container {
|
||||
@ -102,22 +201,6 @@ main {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.btn-signout {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--primary-hover);
|
||||
border-color: var(--primary);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@ -343,3 +426,15 @@ form {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.window-titlebar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.window-titlebar-controls {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,16 +70,44 @@ function Navbar() {
|
||||
h('div', { className: 'profile-info' },
|
||||
h('span', { className: 'profile-label' }, 'Account'),
|
||||
h('span', { className: 'profile-id' }, uid)
|
||||
),
|
||||
h('button', {
|
||||
className: 'btn-signout',
|
||||
onClick: () => sendEvent('bank::close', {})
|
||||
}, 'Sign Out')
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function WindowTitleBar() {
|
||||
return h('div', { className: 'window-titlebar' },
|
||||
h('div', { className: 'window-titlebar-brand' },
|
||||
h('span', { className: 'window-titlebar-kicker' }, 'FDIC Workspace'),
|
||||
h('span', { className: 'window-titlebar-title' }, 'Global Financial Network')
|
||||
),
|
||||
h('div', { className: 'window-titlebar-controls' },
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'window-control-btn',
|
||||
disabled: true,
|
||||
title: 'Minimize unavailable',
|
||||
'aria-label': 'Minimize unavailable'
|
||||
}, '-'),
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'window-control-btn',
|
||||
disabled: true,
|
||||
title: 'Maximize unavailable',
|
||||
'aria-label': 'Maximize unavailable'
|
||||
}, '[ ]'),
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'window-control-btn is-close',
|
||||
onClick: () => sendEvent('bank::close', {}),
|
||||
title: 'Close',
|
||||
'aria-label': 'Close banking interface'
|
||||
}, 'X')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TransactionHistory() {
|
||||
const state = store.getState();
|
||||
const transactions = state.transactions || [];
|
||||
@ -295,12 +323,15 @@ function Footer() {
|
||||
}
|
||||
|
||||
function App() {
|
||||
return h('main', null,
|
||||
Navbar(),
|
||||
h('div', { className: 'container' },
|
||||
BankDashboard()
|
||||
),
|
||||
Footer()
|
||||
return h('div', { className: 'app-shell' },
|
||||
WindowTitleBar(),
|
||||
h('main', null,
|
||||
Navbar(),
|
||||
h('div', { className: 'container' },
|
||||
BankDashboard()
|
||||
),
|
||||
Footer()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
9
arma/client/addons/notifications/CfgSounds.hpp
Normal file
9
arma/client/addons/notifications/CfgSounds.hpp
Normal file
@ -0,0 +1,9 @@
|
||||
class CfgSounds {
|
||||
sounds[] += {QGVAR(notify)};
|
||||
|
||||
class GVAR(notify) {
|
||||
name = QGVAR(notify);
|
||||
sound[] = {QPATHTOF2(sounds\notify.ogg), 1, 1};
|
||||
titles[] = {};
|
||||
};
|
||||
};
|
||||
@ -11,5 +11,6 @@
|
||||
[QGVAR(recieveNotification), {
|
||||
params [["_type", "", [""]], ["_title", "", [""]], ["_content", "", [""]], ["_duration", 4000, [4000]]];
|
||||
|
||||
playSound QGVAR(notify);
|
||||
GVAR(NotificationClass) call ["create", [_type, _title, _content, _duration]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
@ -16,6 +16,7 @@ class CfgPatches {
|
||||
};
|
||||
};
|
||||
|
||||
#include "CfgSounds.hpp"
|
||||
#include "CfgEventHandlers.hpp"
|
||||
#include "ui\RscCommon.hpp"
|
||||
#include "ui\RscNotifications.hpp"
|
||||
|
||||
BIN
arma/client/addons/notifications/sounds/notify.ogg
Normal file
BIN
arma/client/addons/notifications/sounds/notify.ogg
Normal file
Binary file not shown.
@ -1,35 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Forge - Notification System</title>
|
||||
<!-- <link rel="stylesheet" href="styles.css"> -->
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
This approach is used instead of static HTML imports to work with Arma 3's file system
|
||||
-->
|
||||
<script>
|
||||
Promise.all([
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\notifications\\ui\\_site\\styles.css",
|
||||
),
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\notifications\\ui\\_site\\script.js",
|
||||
),
|
||||
]).then(([css, js]) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Forge - Notification System</title>
|
||||
<!-- <link rel="stylesheet" href="styles.css"> -->
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
This approach is used instead of static HTML imports to work with Arma 3's file system
|
||||
-->
|
||||
<script>
|
||||
Promise.all([
|
||||
A3API.RequestFile("forge\\forge_client\\addons\\notifications\\ui\\_site\\styles.css"),
|
||||
A3API.RequestFile("forge\\forge_client\\addons\\notifications\\ui\\_site\\script.js")
|
||||
]).then(([css, js]) => {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="notification-container" class="notification-container" role="region" aria-label="Notifications"></div>
|
||||
<!-- <script src="script.js"></script> -->
|
||||
</body>
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="notifications-hud" aria-live="polite" aria-atomic="false">
|
||||
<div
|
||||
id="notification-container"
|
||||
class="notification-container"
|
||||
role="region"
|
||||
aria-label="Notifications"
|
||||
></div>
|
||||
</div>
|
||||
<!-- <script src="script.js"></script> -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
//=============================================================================
|
||||
|
||||
const NotificationActionTypes = {
|
||||
ADD_NOTIFICATION: 'ADD_NOTIFICATION',
|
||||
REMOVE_NOTIFICATION: 'REMOVE_NOTIFICATION',
|
||||
CLEAR_NOTIFICATIONS: 'CLEAR_NOTIFICATIONS',
|
||||
UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION'
|
||||
ADD_NOTIFICATION: "ADD_NOTIFICATION",
|
||||
REMOVE_NOTIFICATION: "REMOVE_NOTIFICATION",
|
||||
CLEAR_NOTIFICATIONS: "CLEAR_NOTIFICATIONS",
|
||||
UPDATE_NOTIFICATION: "UPDATE_NOTIFICATION",
|
||||
};
|
||||
|
||||
const notificationActions = {
|
||||
@ -15,25 +15,25 @@ const notificationActions = {
|
||||
payload: {
|
||||
id: Date.now() + Math.random(),
|
||||
timestamp: Date.now(),
|
||||
type: 'info',
|
||||
title: 'Notification',
|
||||
message: 'Default message',
|
||||
type: "info",
|
||||
title: "Notification",
|
||||
message: "Default message",
|
||||
duration: 0,
|
||||
status: 'showing',
|
||||
...notification
|
||||
}
|
||||
status: "showing",
|
||||
...notification,
|
||||
},
|
||||
}),
|
||||
removeNotification: (id) => ({
|
||||
type: NotificationActionTypes.REMOVE_NOTIFICATION,
|
||||
payload: { id }
|
||||
payload: { id },
|
||||
}),
|
||||
clearNotifications: () => ({
|
||||
type: NotificationActionTypes.CLEAR_NOTIFICATIONS
|
||||
type: NotificationActionTypes.CLEAR_NOTIFICATIONS,
|
||||
}),
|
||||
updateNotification: (id, updates) => ({
|
||||
type: NotificationActionTypes.UPDATE_NOTIFICATION,
|
||||
payload: { id, updates }
|
||||
})
|
||||
payload: { id, updates },
|
||||
}),
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
@ -42,7 +42,7 @@ const notificationActions = {
|
||||
|
||||
const notificationInitialState = {
|
||||
notifications: [],
|
||||
maxNotifications: 5
|
||||
maxNotifications: 3,
|
||||
};
|
||||
|
||||
function notificationReducer(state = notificationInitialState, action = {}) {
|
||||
@ -53,24 +53,36 @@ function notificationReducer(state = notificationInitialState, action = {}) {
|
||||
if (newNotifications.length >= state.maxNotifications) {
|
||||
newNotifications = newNotifications.slice(1);
|
||||
}
|
||||
return { ...state, notifications: [...newNotifications, action.payload] };
|
||||
return {
|
||||
...state,
|
||||
notifications: [...newNotifications, action.payload],
|
||||
};
|
||||
}
|
||||
case NotificationActionTypes.REMOVE_NOTIFICATION: {
|
||||
if (!action.payload || !action.payload.id) return state;
|
||||
return {
|
||||
...state,
|
||||
notifications: state.notifications.filter(n => n.id !== action.payload.id)
|
||||
notifications: state.notifications.filter(
|
||||
(n) => n.id !== action.payload.id,
|
||||
),
|
||||
};
|
||||
}
|
||||
case NotificationActionTypes.CLEAR_NOTIFICATIONS:
|
||||
return { ...state, notifications: [] };
|
||||
case NotificationActionTypes.UPDATE_NOTIFICATION: {
|
||||
if (!action.payload || !action.payload.id || !action.payload.updates) return state;
|
||||
if (
|
||||
!action.payload ||
|
||||
!action.payload.id ||
|
||||
!action.payload.updates
|
||||
)
|
||||
return state;
|
||||
return {
|
||||
...state,
|
||||
notifications: state.notifications.map(n =>
|
||||
n.id === action.payload.id ? { ...n, ...action.payload.updates } : n
|
||||
)
|
||||
notifications: state.notifications.map((n) =>
|
||||
n.id === action.payload.id
|
||||
? { ...n, ...action.payload.updates }
|
||||
: n,
|
||||
),
|
||||
};
|
||||
}
|
||||
default:
|
||||
@ -95,19 +107,22 @@ class Store {
|
||||
|
||||
dispatch(action) {
|
||||
this.state = this.reducer(this.state, action);
|
||||
this.listeners.forEach(listener => listener(this.state));
|
||||
this.listeners.forEach((listener) => listener(this.state));
|
||||
return action;
|
||||
}
|
||||
|
||||
subscribe(listener) {
|
||||
this.listeners.push(listener);
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter(l => l !== listener);
|
||||
this.listeners = this.listeners.filter((l) => l !== listener);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const notificationStore = new Store(notificationReducer, notificationInitialState);
|
||||
const notificationStore = new Store(
|
||||
notificationReducer,
|
||||
notificationInitialState,
|
||||
);
|
||||
|
||||
//=============================================================================
|
||||
// #region SELECTORS
|
||||
@ -115,7 +130,7 @@ const notificationStore = new Store(notificationReducer, notificationInitialStat
|
||||
|
||||
const notificationSelectors = {
|
||||
getNotifications: (state) => state.notifications,
|
||||
getMaxNotifications: (state) => state.maxNotifications
|
||||
getMaxNotifications: (state) => state.maxNotifications,
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
@ -126,13 +141,14 @@ class NotificationUI {
|
||||
constructor(store) {
|
||||
this.store = store;
|
||||
this.unsubscribe = null;
|
||||
this.container = document.getElementById('notification-container');
|
||||
this.container = document.getElementById("notification-container");
|
||||
this.renderedNotifications = new Map();
|
||||
this.dismissTimers = new Map();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.container) {
|
||||
console.error('Notification container not found');
|
||||
console.error("Notification container not found");
|
||||
return;
|
||||
}
|
||||
this.unsubscribe = this.store.subscribe((state) => this.render(state));
|
||||
@ -141,7 +157,13 @@ class NotificationUI {
|
||||
|
||||
destroy() {
|
||||
if (this.unsubscribe) this.unsubscribe();
|
||||
this.renderedNotifications.forEach(el => {
|
||||
this.dismissTimers.forEach((timers) => {
|
||||
clearTimeout(timers.hideTimer);
|
||||
clearTimeout(timers.removeTimer);
|
||||
clearTimeout(timers.progressTimer);
|
||||
});
|
||||
this.dismissTimers.clear();
|
||||
this.renderedNotifications.forEach((el) => {
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
this.renderedNotifications.clear();
|
||||
@ -151,16 +173,17 @@ class NotificationUI {
|
||||
const notifications = notificationSelectors.getNotifications(state);
|
||||
|
||||
// Remove notifications no longer present
|
||||
const currentIds = new Set(notifications.map(n => n.id));
|
||||
const currentIds = new Set(notifications.map((n) => n.id));
|
||||
for (const [id, el] of this.renderedNotifications.entries()) {
|
||||
if (!currentIds.has(id)) {
|
||||
this.clearDismissTimers(id);
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
this.renderedNotifications.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update notifications
|
||||
notifications.forEach(notification => {
|
||||
notifications.forEach((notification) => {
|
||||
if (!notification || !notification.id) return;
|
||||
if (!this.renderedNotifications.has(notification.id)) {
|
||||
this.createNotificationElement(notification);
|
||||
@ -170,45 +193,133 @@ class NotificationUI {
|
||||
});
|
||||
}
|
||||
|
||||
clearDismissTimers(id) {
|
||||
const timers = this.dismissTimers.get(id);
|
||||
if (!timers) return;
|
||||
|
||||
clearTimeout(timers.hideTimer);
|
||||
clearTimeout(timers.removeTimer);
|
||||
clearTimeout(timers.progressTimer);
|
||||
this.dismissTimers.delete(id);
|
||||
}
|
||||
|
||||
escapeHTML(value) {
|
||||
return String(value == null ? "" : value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
normalizeType(type) {
|
||||
const supportedTypes = new Set([
|
||||
"success",
|
||||
"danger",
|
||||
"warning",
|
||||
"info",
|
||||
]);
|
||||
return supportedTypes.has(type) ? type : "info";
|
||||
}
|
||||
|
||||
formatTypeLabel(type) {
|
||||
const labels = {
|
||||
success: "Success",
|
||||
danger: "Critical",
|
||||
warning: "Warning",
|
||||
info: "Info",
|
||||
};
|
||||
return labels[this.normalizeType(type)] || labels.info;
|
||||
}
|
||||
|
||||
getDurationLabel(duration) {
|
||||
if (!(duration > 0)) return "Pinned";
|
||||
const seconds = Math.max(1, Math.round(duration / 100) / 10);
|
||||
return `${seconds.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
getTimestampLabel(timestamp) {
|
||||
const date = new Date(timestamp || Date.now());
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
createNotificationElement(notification) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `notification ${notification.type || 'info'}`;
|
||||
const type = this.normalizeType(notification.type);
|
||||
const title = this.escapeHTML(notification.title || "Notification");
|
||||
const message = this.escapeHTML(notification.message || "No message");
|
||||
const isPersistent = !(notification.duration > 0);
|
||||
const el = document.createElement("div");
|
||||
el.className = `notification ${type}${isPersistent ? " is-persistent" : ""}`;
|
||||
el.dataset.id = notification.id;
|
||||
el.innerHTML = `
|
||||
<div class="notification-header">
|
||||
<div class="notification-title">${notification.title || 'Notification'}</div>
|
||||
<div class="notification-inner">
|
||||
<div class="notification-header">
|
||||
<div class="notification-title">${title}</div>
|
||||
<div class="notification-subtitle">Forge alert</div>
|
||||
</div>
|
||||
<div class="notification-body">
|
||||
<div class="notification-meta">
|
||||
<span class="notification-badge">${this.formatTypeLabel(type)}</span>
|
||||
<span class="notification-time">${this.getTimestampLabel(notification.timestamp)}</span>
|
||||
</div>
|
||||
<div class="notification-message">${message}</div>
|
||||
<div class="notification-footer">
|
||||
<span>${isPersistent ? "Persistent signal" : "Auto-dismiss"}</span>
|
||||
<span>${this.getDurationLabel(notification.duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification-message">${notification.message || 'No message'}</div>
|
||||
${notification.duration > 0 ? '<div class="notification-progress"><div class="notification-progress-bar"></div></div>' : ''}
|
||||
${notification.duration > 0 ? '<div class="notification-progress"><div class="notification-progress-bar"></div></div>' : ""}
|
||||
`;
|
||||
this.container.appendChild(el);
|
||||
this.renderedNotifications.set(notification.id, el);
|
||||
setTimeout(() => el.classList.add('show'), 10);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => el.classList.add("show"));
|
||||
});
|
||||
|
||||
// Set progress bar animation duration
|
||||
if (notification.duration > 0) {
|
||||
const progressBar = el.querySelector('.notification-progress-bar');
|
||||
const progressBar = el.querySelector(".notification-progress-bar");
|
||||
if (progressBar) {
|
||||
progressBar.style.transition = `width ${notification.duration}ms linear`;
|
||||
progressBar.style.width = '100%';
|
||||
setTimeout(() => {
|
||||
progressBar.style.width = '0%';
|
||||
}, 20);
|
||||
progressBar.style.transitionDuration = `${notification.duration}ms`;
|
||||
const progressTimer = setTimeout(() => {
|
||||
progressBar.style.transform = "scaleX(0)";
|
||||
}, 30);
|
||||
this.dismissTimers.set(notification.id, { progressTimer });
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
notificationStore.dispatch(notificationActions.updateNotification(notification.id, { status: 'hiding' }));
|
||||
setTimeout(() => {
|
||||
notificationStore.dispatch(notificationActions.removeNotification(notification.id));
|
||||
}, 300);
|
||||
const hideTimer = setTimeout(() => {
|
||||
notificationStore.dispatch(
|
||||
notificationActions.updateNotification(notification.id, {
|
||||
status: "hiding",
|
||||
}),
|
||||
);
|
||||
}, notification.duration);
|
||||
const removeTimer = setTimeout(() => {
|
||||
this.clearDismissTimers(notification.id);
|
||||
notificationStore.dispatch(
|
||||
notificationActions.removeNotification(notification.id),
|
||||
);
|
||||
}, notification.duration + 260);
|
||||
|
||||
const existingTimers =
|
||||
this.dismissTimers.get(notification.id) || {};
|
||||
this.dismissTimers.set(notification.id, {
|
||||
...existingTimers,
|
||||
hideTimer,
|
||||
removeTimer,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateNotificationElement(notification) {
|
||||
const el = this.renderedNotifications.get(notification.id);
|
||||
if (!el) return;
|
||||
if (notification.status === 'hiding') el.classList.add('hide');
|
||||
if (notification.status === "hiding") {
|
||||
el.classList.add("hide");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,7 +331,11 @@ let notificationUI = null;
|
||||
let notificationUIInitialized = false;
|
||||
|
||||
function notifyArmaNotificationReady() {
|
||||
if (window.parent && window.parent !== window && typeof window.parent.postMessage === "function") {
|
||||
if (
|
||||
window.parent &&
|
||||
window.parent !== window &&
|
||||
typeof window.parent.postMessage === "function"
|
||||
) {
|
||||
window.parent.postMessage({ event: "notifications::ready" }, "*");
|
||||
}
|
||||
if (typeof A3API !== "undefined" && typeof A3API.SendAlert === "function") {
|
||||
@ -230,34 +345,44 @@ function notifyArmaNotificationReady() {
|
||||
|
||||
function initializeNotifications() {
|
||||
if (notificationUIInitialized) {
|
||||
console.log('Notification system already initialized, skipping...');
|
||||
console.log("Notification system already initialized, skipping...");
|
||||
return;
|
||||
}
|
||||
notificationUI = new NotificationUI(notificationStore);
|
||||
notificationUI.init();
|
||||
notificationUIInitialized = true;
|
||||
console.log('Notification system is ready!');
|
||||
console.log("Notification system is ready!");
|
||||
notifyArmaNotificationReady();
|
||||
}
|
||||
|
||||
// Expose global notification API
|
||||
const showNotification = (type, title, message, duration) => {
|
||||
return notificationStore.dispatch(notificationActions.addNotification({ type, title, message, duration }));
|
||||
return notificationStore.dispatch(
|
||||
notificationActions.addNotification({ type, title, message, duration }),
|
||||
);
|
||||
};
|
||||
const clearAllNotifications = () => {
|
||||
return notificationStore.dispatch(notificationActions.clearNotifications());
|
||||
};
|
||||
window.showNotification = showNotification;
|
||||
window.clearAllNotifications = clearAllNotifications;
|
||||
window.ForgeNotifications = {
|
||||
show: showNotification,
|
||||
clear: clearAllNotifications,
|
||||
};
|
||||
|
||||
// Listen for global notification events (for Arma/SQF or other scripts)
|
||||
window.addEventListener('forge:notify', function (e) {
|
||||
window.addEventListener("forge:notify", function (e) {
|
||||
if (!e || !e.detail) return;
|
||||
const { type, title, message, duration } = e.detail;
|
||||
showNotification(type, title, message, duration);
|
||||
});
|
||||
|
||||
// Auto-initialize if DOM is already loaded when script executes
|
||||
if (document.readyState !== 'loading') {
|
||||
if (document.readyState !== "loading") {
|
||||
initializeNotifications();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', initializeNotifications, { once: true });
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", initializeNotifications, {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,26 +1,27 @@
|
||||
:root {
|
||||
--bg-surface: #ffffff;
|
||||
--bg-surface-hover: #f1f5f9;
|
||||
--primary: #475569;
|
||||
--primary-hover: #1e293b;
|
||||
--text-main: #1f2937;
|
||||
--text-muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--radius: 8px;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
|
||||
--success: #10b981;
|
||||
--success-bg: #ecfdf5;
|
||||
--success-border: #a7f3d0;
|
||||
--danger: #ef4444;
|
||||
--danger-bg: #fef2f2;
|
||||
--danger-border: #fecaca;
|
||||
--warning: #f59e0b;
|
||||
--warning-bg: #fffbeb;
|
||||
--warning-border: #fde68a;
|
||||
--info: #3b82f6;
|
||||
--info-bg: #eff6ff;
|
||||
--info-border: #bfdbfe;
|
||||
--hud-top: 30px;
|
||||
--hud-right: 20px;
|
||||
--header-bg: linear-gradient(
|
||||
180deg,
|
||||
rgba(13, 37, 69, 0.98) 0%,
|
||||
rgba(8, 24, 48, 0.98) 100%
|
||||
);
|
||||
--body-bg: rgba(242, 238, 228, 0.97);
|
||||
--panel-edge: rgba(121, 166, 212, 0.22);
|
||||
--panel-shadow: 0 14px 24px rgba(0, 0, 0, 0.34);
|
||||
--header-text: rgba(235, 243, 255, 0.98);
|
||||
--header-muted: rgba(166, 189, 221, 0.8);
|
||||
--body-text: rgba(31, 45, 64, 0.96);
|
||||
--body-muted: rgba(86, 102, 122, 0.82);
|
||||
--body-faint: rgba(111, 126, 144, 0.76);
|
||||
--success: #6de2b3;
|
||||
--success-soft: rgba(109, 226, 179, 0.12);
|
||||
--danger: #ff7b7b;
|
||||
--danger-soft: rgba(255, 123, 123, 0.12);
|
||||
--warning: #ffd36b;
|
||||
--warning-soft: rgba(255, 211, 107, 0.12);
|
||||
--info: #78b9ff;
|
||||
--info-soft: rgba(120, 185, 255, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
@ -29,182 +30,216 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background: transparent;
|
||||
min-height: 100vh;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
body {
|
||||
background: transparent;
|
||||
color: var(--body-text);
|
||||
font-family: "Bahnschrift", "Segoe UI", Tahoma, sans-serif;
|
||||
letter-spacing: 0.01em;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.notifications-hud {
|
||||
position: fixed;
|
||||
top: var(--hud-top);
|
||||
right: var(--hud-right);
|
||||
width: 384px;
|
||||
max-width: calc(100vw - 24px);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 24px;
|
||||
z-index: 1000;
|
||||
width: 360px;
|
||||
pointer-events: auto;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.notification {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 4px solid var(--primary);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 0.1), 0 2px 4px rgb(0 0 0 / 0.05);
|
||||
margin-bottom: 12px;
|
||||
padding: 1rem 1.25rem;
|
||||
width: 100%;
|
||||
transform: translateX(calc(100% + 24px));
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: 1px solid var(--panel-edge);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--panel-shadow);
|
||||
opacity: 0;
|
||||
transform: translateX(28px) scale(0.985);
|
||||
transition:
|
||||
opacity 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
border-color 0.18s ease;
|
||||
}
|
||||
|
||||
&.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.notification::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.08),
|
||||
transparent 24%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.hide {
|
||||
transform: translateX(calc(100% + 24px));
|
||||
opacity: 0;
|
||||
}
|
||||
.notification.show {
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: var(--success-bg);
|
||||
border-color: var(--success-border);
|
||||
border-left-color: var(--success);
|
||||
.notification.hide {
|
||||
opacity: 0;
|
||||
transform: translateX(32px) scale(0.985);
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
color: #065f46;
|
||||
}
|
||||
.notification-inner {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: #047857;
|
||||
}
|
||||
.notification-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
padding: 10px 11px 11px;
|
||||
background: var(--body-bg);
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background: var(--danger-bg);
|
||||
border-color: var(--danger-border);
|
||||
border-left-color: var(--danger);
|
||||
|
||||
.notification-title {
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: var(--warning-bg);
|
||||
border-color: var(--warning-border);
|
||||
border-left-color: var(--warning);
|
||||
|
||||
.notification-title {
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: var(--info-bg);
|
||||
border-color: var(--info-border);
|
||||
border-left-color: var(--info);
|
||||
|
||||
.notification-title {
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: var(--info);
|
||||
}
|
||||
}
|
||||
.notification-meta,
|
||||
.notification-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.notification-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.375rem;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px 11px;
|
||||
background: var(--header-bg);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.notification-meta {
|
||||
color: var(--body-muted);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
padding: 3px 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid currentColor;
|
||||
background: rgba(34, 51, 74, 0.08);
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
color: var(--body-faint);
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--header-text);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.notification-subtitle {
|
||||
color: var(--header-muted);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
flex: 1;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
color: var(--body-text);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.notification-footer {
|
||||
color: var(--body-faint);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.notification-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: var(--bg-surface-hover);
|
||||
width: 100%;
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
height: 4px;
|
||||
background: rgba(34, 51, 74, 0.16);
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
transform-origin: left;
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
transition: width linear;
|
||||
width: 100%;
|
||||
background: currentColor;
|
||||
opacity: 0.95;
|
||||
transform: scaleX(1);
|
||||
transform-origin: left center;
|
||||
transition: transform linear;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.notification-container {
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.notification {
|
||||
transform: translateY(-100%);
|
||||
|
||||
&.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.hide {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
.notification.is-persistent .notification-progress {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.notification-container {
|
||||
width: calc(100vw - 48px);
|
||||
.notification.success {
|
||||
color: var(--success);
|
||||
border-color: rgba(109, 226, 179, 0.24);
|
||||
}
|
||||
|
||||
.notification.success .notification-badge {
|
||||
background-color: var(--success-soft);
|
||||
}
|
||||
|
||||
.notification.danger {
|
||||
color: var(--danger);
|
||||
border-color: rgba(255, 123, 123, 0.24);
|
||||
}
|
||||
|
||||
.notification.danger .notification-badge {
|
||||
background-color: var(--danger-soft);
|
||||
}
|
||||
|
||||
.notification.warning {
|
||||
color: var(--warning);
|
||||
border-color: rgba(255, 211, 107, 0.24);
|
||||
}
|
||||
|
||||
.notification.warning .notification-badge {
|
||||
background-color: var(--warning-soft);
|
||||
}
|
||||
|
||||
.notification.info {
|
||||
color: var(--info);
|
||||
border-color: rgba(120, 185, 255, 0.24);
|
||||
}
|
||||
|
||||
.notification.info .notification-badge {
|
||||
background-color: var(--info-soft);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
:root {
|
||||
--hud-top: 18px;
|
||||
--hud-right: 12px;
|
||||
}
|
||||
|
||||
.notifications-hud {
|
||||
width: calc(100vw - 16px);
|
||||
max-width: calc(100vw - 16px);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
--bg-surface-hover: #f1f5f9;
|
||||
--primary: #475569;
|
||||
--primary-hover: #1e293b;
|
||||
--window-blue: #12325b;
|
||||
--window-blue-border: #214978;
|
||||
--window-blue-highlight: #d7e5f8;
|
||||
--text-main: #1f2937;
|
||||
--text-muted: #64748b;
|
||||
--text-inverse: #f8fafc;
|
||||
@ -13,6 +16,11 @@
|
||||
--footer-bg: #1e293b;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
"Inter",
|
||||
@ -24,16 +32,107 @@ body {
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
line-height: 1.6;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.window-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.8rem 1.25rem;
|
||||
background: linear-gradient(180deg, var(--window-blue) 0%, #0d2643 100%);
|
||||
border-bottom: 1px solid var(--window-blue-border);
|
||||
color: var(--text-inverse);
|
||||
box-shadow: 0 10px 24px rgb(18 50 91 / 0.24);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.window-titlebar-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.window-titlebar-kicker {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: rgb(215 229 248 / 0.78);
|
||||
}
|
||||
|
||||
.window-titlebar-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.window-titlebar-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.window-control-btn {
|
||||
min-width: 2.5rem;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgb(215 229 248 / 0.22);
|
||||
background: rgb(255 255 255 / 0.08);
|
||||
color: var(--window-blue-highlight);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.window-control-btn:hover {
|
||||
background: rgb(255 255 255 / 0.08);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.window-control-btn:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.window-control-btn.is-close {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
border-color: rgb(255 255 255 / 0.24);
|
||||
}
|
||||
|
||||
.window-control-btn.is-close:hover {
|
||||
background: rgb(255 255 255 / 0.18);
|
||||
}
|
||||
|
||||
.container {
|
||||
@ -156,6 +255,16 @@ button {
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.window-titlebar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.window-titlebar-controls {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
@ -5,6 +5,60 @@
|
||||
|
||||
RegistryApp.components = RegistryApp.components || {};
|
||||
|
||||
function WindowTitleBar({ title, onClose }) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "window-titlebar" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "window-titlebar-brand" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "window-titlebar-kicker" },
|
||||
"ORBIS Workspace",
|
||||
),
|
||||
h("span", { className: "window-titlebar-title" }, title),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "window-titlebar-controls" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "window-control-btn",
|
||||
disabled: true,
|
||||
title: "Minimize unavailable",
|
||||
"aria-label": "Minimize unavailable",
|
||||
},
|
||||
"-",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "window-control-btn",
|
||||
disabled: true,
|
||||
title: "Maximize unavailable",
|
||||
"aria-label": "Maximize unavailable",
|
||||
},
|
||||
"[ ]",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "window-control-btn is-close",
|
||||
onClick: onClose,
|
||||
title: "Close",
|
||||
"aria-label": "Close organization interface",
|
||||
},
|
||||
"X",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
RegistryApp.components.App = function App() {
|
||||
const Navbar = window.SharedUI.componentFns.Navbar;
|
||||
const Header = window.SharedUI.componentFns.Header;
|
||||
@ -23,7 +77,6 @@
|
||||
: view === "portal"
|
||||
? "Organization Portal"
|
||||
: "Entry Hub";
|
||||
const actionLabel = view === "portal" ? "Sign Out" : "Close";
|
||||
const footerSections = [
|
||||
{
|
||||
title: "Registry Resources",
|
||||
@ -65,12 +118,14 @@
|
||||
if (view === "portal" && PortalApp) {
|
||||
return h(
|
||||
"div",
|
||||
null,
|
||||
{ className: "app-shell" },
|
||||
WindowTitleBar({
|
||||
title: "Global Organization Network",
|
||||
onClose: closeRegistry,
|
||||
}),
|
||||
Navbar({
|
||||
title: "Global Organization Network",
|
||||
viewLabel,
|
||||
actionLabel,
|
||||
onAction: closeRegistry,
|
||||
}),
|
||||
PortalApp(),
|
||||
);
|
||||
@ -84,24 +139,30 @@
|
||||
}
|
||||
|
||||
return h(
|
||||
"main",
|
||||
null,
|
||||
Navbar({
|
||||
"div",
|
||||
{ className: "app-shell" },
|
||||
WindowTitleBar({
|
||||
title: "Global Organization Network",
|
||||
viewLabel,
|
||||
actionLabel,
|
||||
onAction: closeRegistry,
|
||||
onClose: closeRegistry,
|
||||
}),
|
||||
h(
|
||||
"div",
|
||||
{ className: "container" },
|
||||
Header({
|
||||
"main",
|
||||
null,
|
||||
Navbar({
|
||||
title: "Global Organization Network",
|
||||
onTitleClick: () => store.setView("home"),
|
||||
viewLabel,
|
||||
}),
|
||||
mainContent,
|
||||
h(
|
||||
"div",
|
||||
{ className: "container" },
|
||||
Header({
|
||||
title: "Global Organization Network",
|
||||
onTitleClick: () => store.setView("home"),
|
||||
}),
|
||||
mainContent,
|
||||
),
|
||||
Footer({ sections: footerSections }),
|
||||
),
|
||||
Footer({ sections: footerSections }),
|
||||
);
|
||||
};
|
||||
})();
|
||||
|
||||
@ -113,15 +113,17 @@ ${scopeSelector} .app-close-btn:hover {
|
||||
"div",
|
||||
{ className: "app-navbar-actions" },
|
||||
h("span", { className: "app-navbar-view" }, viewLabel),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "app-close-btn",
|
||||
onClick: onAction,
|
||||
},
|
||||
actionLabel,
|
||||
),
|
||||
actionLabel && typeof onAction === "function"
|
||||
? h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "app-close-btn",
|
||||
onClick: onAction,
|
||||
},
|
||||
actionLabel,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
BIN
arma/client/extra/notify.wav
vendored
Normal file
BIN
arma/client/extra/notify.wav
vendored
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user