glad-web 1.0.35 → 1.0.37
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.
- package/lib/codex/structured-session.js +57 -49
- package/lib/commands/web.js +23 -2
- package/lib/config/manager.js +18 -0
- package/lib/notifications/message-formatter.js +94 -0
- package/lib/notifications/notification-service.js +143 -0
- package/lib/notifications/serverchan-client.js +58 -0
- package/lib/notifications/serverchan-settings-store.js +115 -0
- package/lib/server/routes/notifications.js +52 -0
- package/lib/session/session-manager.js +1 -0
- package/lib/web/codex.js +130 -43
- package/lib/web/core.js +6 -5
- package/lib/web/index.html +60 -6
- package/lib/web/notifications.js +162 -0
- package/lib/web/schedules.js +1 -1
- package/lib/web/session.js +8 -8
- package/lib/web/styles.css +33 -1
- package/package.json +1 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
let serverChanSettings = null;
|
|
2
|
+
let serverChanToastTimer = null;
|
|
3
|
+
|
|
4
|
+
function serverChanBellSvg() {
|
|
5
|
+
return '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"></path><path d="M10 21h4"></path></svg>';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function renderServerChanSessionAction(session) {
|
|
9
|
+
const enabled = Boolean(session.serverChanNotificationEnabled);
|
|
10
|
+
const title = enabled ? 'Disable ServerChan notifications for this chat' : 'Enable ServerChan notifications for this chat';
|
|
11
|
+
return `<button class="serverchan-toggle${enabled ? ' active' : ''}" type="button"
|
|
12
|
+
data-serverchan-session="${escapeHtml(session.id)}"
|
|
13
|
+
aria-label="${title}" title="${title}"
|
|
14
|
+
onclick="toggleServerChanSession('${session.id}', ${enabled ? 'false' : 'true'}, event)">
|
|
15
|
+
${serverChanBellSvg()}
|
|
16
|
+
</button>`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function showAppToast(message) {
|
|
20
|
+
const toast = document.getElementById('app-toast');
|
|
21
|
+
toast.textContent = message;
|
|
22
|
+
toast.classList.add('visible');
|
|
23
|
+
clearTimeout(serverChanToastTimer);
|
|
24
|
+
serverChanToastTimer = setTimeout(() => toast.classList.remove('visible'), 1800);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function toggleServerChanSession(sessionId, enabled, event) {
|
|
28
|
+
event?.stopPropagation();
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetchWithTimeout(`/api/sessions/${sessionId}/notifications/serverchan`, {
|
|
31
|
+
method: 'PUT',
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({ enabled })
|
|
34
|
+
}, 10000);
|
|
35
|
+
const data = await response.json();
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
if (data.code === 'SERVERCHAN_NOT_CONFIGURED') {
|
|
38
|
+
showAppToast('Configure ServerChan in Settings first');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
throw new Error(data.error || 'Could not update notifications');
|
|
42
|
+
}
|
|
43
|
+
await refreshSessionsNow();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
showAppToast(error.message || 'Could not update notifications');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function openSettings(event = null) {
|
|
50
|
+
event?.stopPropagation();
|
|
51
|
+
document.getElementById('settings-modal-overlay').style.display = 'flex';
|
|
52
|
+
setServerChanStatus('Loading configuration…');
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetchWithTimeout('/api/notifications/serverchan', {}, 10000);
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
if (!response.ok) throw new Error(data.error || 'Could not load configuration');
|
|
57
|
+
serverChanSettings = data;
|
|
58
|
+
document.getElementById('serverchan-client-type').value = data.clientType || 'wechat';
|
|
59
|
+
const keyInput = document.getElementById('serverchan-send-key');
|
|
60
|
+
keyInput.value = '';
|
|
61
|
+
keyInput.placeholder = data.configured ? data.maskedKey : 'SCT...';
|
|
62
|
+
document.getElementById('serverchan-key-hint').textContent = data.configured
|
|
63
|
+
? `Saved: ${data.maskedKey}. Leave blank to keep the current SendKey.`
|
|
64
|
+
: 'The SendKey is stored only on this Glad host.';
|
|
65
|
+
document.getElementById('serverchan-remove-btn').style.display = data.configured ? 'inline-flex' : 'none';
|
|
66
|
+
setServerChanStatus(data.configured ? 'Configuration saved.' : 'ServerChan is not configured.');
|
|
67
|
+
} catch (error) {
|
|
68
|
+
setServerChanStatus(error.message || 'Could not load configuration', 'error');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function closeSettings(event = null) {
|
|
73
|
+
if (event && event.target.id !== 'settings-modal-overlay') return;
|
|
74
|
+
document.getElementById('settings-modal-overlay').style.display = 'none';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function currentServerChanForm() {
|
|
78
|
+
return {
|
|
79
|
+
sendKey: document.getElementById('serverchan-send-key').value.trim(),
|
|
80
|
+
clientType: document.getElementById('serverchan-client-type').value
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function setServerChanStatus(message, type = '') {
|
|
85
|
+
const status = document.getElementById('serverchan-settings-status');
|
|
86
|
+
status.textContent = message;
|
|
87
|
+
status.className = `serverchan-settings-status${type ? ` ${type}` : ''}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function setServerChanBusy(busy) {
|
|
91
|
+
document.getElementById('serverchan-save-btn').disabled = busy;
|
|
92
|
+
document.getElementById('serverchan-test-btn').disabled = busy;
|
|
93
|
+
document.getElementById('serverchan-remove-btn').disabled = busy;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function saveServerChanSettings() {
|
|
97
|
+
setServerChanBusy(true);
|
|
98
|
+
setServerChanStatus('Saving…');
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetchWithTimeout('/api/notifications/serverchan', {
|
|
101
|
+
method: 'PUT',
|
|
102
|
+
headers: { 'Content-Type': 'application/json' },
|
|
103
|
+
body: JSON.stringify(currentServerChanForm())
|
|
104
|
+
}, 10000);
|
|
105
|
+
const data = await response.json();
|
|
106
|
+
if (!response.ok) throw new Error(data.error || 'Could not save configuration');
|
|
107
|
+
serverChanSettings = data.settings;
|
|
108
|
+
const keyInput = document.getElementById('serverchan-send-key');
|
|
109
|
+
keyInput.value = '';
|
|
110
|
+
keyInput.placeholder = data.settings.maskedKey;
|
|
111
|
+
document.getElementById('serverchan-key-hint').textContent =
|
|
112
|
+
`Saved: ${data.settings.maskedKey}. Leave blank to keep the current SendKey.`;
|
|
113
|
+
document.getElementById('serverchan-remove-btn').style.display = 'inline-flex';
|
|
114
|
+
setServerChanStatus('Configuration saved. Per-chat notification switches are unchanged.', 'success');
|
|
115
|
+
} catch (error) {
|
|
116
|
+
setServerChanStatus(error.message || 'Could not save configuration', 'error');
|
|
117
|
+
} finally {
|
|
118
|
+
setServerChanBusy(false);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function testServerChanSettings() {
|
|
123
|
+
setServerChanBusy(true);
|
|
124
|
+
setServerChanStatus('Sending test message…');
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetchWithTimeout('/api/notifications/serverchan/test', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
body: JSON.stringify(currentServerChanForm())
|
|
130
|
+
}, 15000);
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
if (!response.ok) throw new Error(data.error || 'Could not send test message');
|
|
133
|
+
setServerChanStatus('Test message sent. The test did not save configuration.', 'success');
|
|
134
|
+
} catch (error) {
|
|
135
|
+
setServerChanStatus(error.message || 'Could not send test message', 'error');
|
|
136
|
+
} finally {
|
|
137
|
+
setServerChanBusy(false);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function removeServerChanSettings() {
|
|
142
|
+
if (!confirm('Remove ServerChan configuration and disable notifications for every chat?')) return;
|
|
143
|
+
setServerChanBusy(true);
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetchWithTimeout('/api/notifications/serverchan', {
|
|
146
|
+
method: 'DELETE'
|
|
147
|
+
}, 10000);
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
if (!response.ok) throw new Error(data.error || 'Could not remove configuration');
|
|
150
|
+
serverChanSettings = data.settings;
|
|
151
|
+
document.getElementById('serverchan-send-key').value = '';
|
|
152
|
+
document.getElementById('serverchan-send-key').placeholder = 'SCT...';
|
|
153
|
+
document.getElementById('serverchan-key-hint').textContent = 'The SendKey is stored only on this Glad host.';
|
|
154
|
+
document.getElementById('serverchan-remove-btn').style.display = 'none';
|
|
155
|
+
setServerChanStatus('Configuration removed. Notifications are disabled for every chat.', 'success');
|
|
156
|
+
await refreshSessionsNow();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
setServerChanStatus(error.message || 'Could not remove configuration', 'error');
|
|
159
|
+
} finally {
|
|
160
|
+
setServerChanBusy(false);
|
|
161
|
+
}
|
|
162
|
+
}
|
package/lib/web/schedules.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
editingScheduleId = data.id || null;
|
|
36
36
|
editingSteps = (data.steps || []).map(step => ({ ...step }));
|
|
37
37
|
selectedWeekdays = [...(data.schedule.weekdays || [1, 2, 3, 4, 5])];
|
|
38
|
-
document.getElementById('schedule-modal-title').textContent = editingScheduleId ? 'Edit
|
|
38
|
+
document.getElementById('schedule-modal-title').textContent = editingScheduleId ? 'Edit Scheduled Task' : 'New Scheduled Task';
|
|
39
39
|
document.getElementById('schedule-name').value = data.name || 'Scheduled Task';
|
|
40
40
|
document.getElementById('schedule-cwd').value = data.target.workingDirectory || '';
|
|
41
41
|
document.getElementById('schedule-time').value = data.schedule.time || '09:00';
|
package/lib/web/session.js
CHANGED
|
@@ -143,11 +143,12 @@
|
|
|
143
143
|
codexSkillQuery = '';
|
|
144
144
|
codexSkillError = '';
|
|
145
145
|
selectedCodexSkill = null;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
codexDetailRequestSeq = 0;
|
|
147
|
+
codexDetailRequests = new Map();
|
|
148
|
+
codexDetailRevisions = new Map();
|
|
149
|
+
clearTimeout(codexDetailRefreshTimer);
|
|
150
|
+
codexDetailRefreshTimer = null;
|
|
151
|
+
codexDetailRefreshIds = new Set();
|
|
151
152
|
document.getElementById('codex-model-panel').classList.remove('active');
|
|
152
153
|
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
153
154
|
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
@@ -157,7 +158,7 @@
|
|
|
157
158
|
codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canCompact: false, compacting: false, canSwitchToTerminal: false, canSwitchToStructured: false };
|
|
158
159
|
setClaudeModeEnabled(false);
|
|
159
160
|
applyCodexState(codexState);
|
|
160
|
-
|
|
161
|
+
installCodexLazyDetailHandler();
|
|
161
162
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
162
163
|
currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
|
|
163
164
|
currentSocket.onmessage = (e) => {
|
|
@@ -165,10 +166,9 @@
|
|
|
165
166
|
if (msg.type === 'codex-snapshot' && msg.snapshot) {
|
|
166
167
|
codexMessages = msg.snapshot.messages || [];
|
|
167
168
|
codexPendingPermissions = msg.snapshot.pendingPermissions || [];
|
|
168
|
-
applyCodexHistoryPageMeta(msg.snapshot.historyPage);
|
|
169
169
|
applyCodexState(msg.snapshot.state || {});
|
|
170
170
|
}
|
|
171
|
-
if (msg.type === 'codex-
|
|
171
|
+
if (msg.type === 'codex-detail-response' && msg.detail) applyCodexDetailResponse(msg);
|
|
172
172
|
if (msg.type === 'codex-event') {
|
|
173
173
|
applyCodexEvent(msg.event);
|
|
174
174
|
if (msg.event?.type === 'presentation' && msg.event.presentation === 'terminal') {
|
package/lib/web/styles.css
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
|
7
7
|
.header h1 { font-size: 28px; font-weight: 700; margin: 0; display: flex; align-items: center; gap: 10px; }
|
|
8
8
|
.header-logo { width: 32px; height: 32px; flex-shrink: 0; }
|
|
9
|
-
.
|
|
9
|
+
.header-actions { display: flex; align-items: center; gap: 5px; }
|
|
10
|
+
.header-action-btn { height: 36px; min-width: 36px; padding: 0 10px; border: 0; border-radius: 10px; background: var(--primary); color: #fff; display: inline-flex; align-items: center; justify-content: center; gap: 3px; font-size: 13px; font-weight: 750; line-height: 1; white-space: nowrap; cursor: pointer; flex-shrink: 0; }
|
|
11
|
+
.header-action-btn.icon-only { width: 36px; padding: 0; }
|
|
12
|
+
.header-action-btn:active { background: #0062cc; transform: scale(.97); }
|
|
10
13
|
.btn-retry { background: #333; color: #fff; border: none; padding: 8px 16px; border-radius: 20px; margin-top: 10px; cursor: pointer; }
|
|
11
14
|
.session-card { background: var(--card-bg); border-radius: 12px; padding: 16px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; transition: transform 0.1s; position: relative; }
|
|
12
15
|
.session-card:active { transform: scale(0.98); }
|
|
@@ -20,12 +23,39 @@
|
|
|
20
23
|
.copy-dir-btn { color: var(--text-dim); background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; width: 28px; height: 28px; padding: 0; display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; }
|
|
21
24
|
.copy-dir-btn:active { color: #fff; background: rgba(255,255,255,0.12); }
|
|
22
25
|
.session-actions { display: flex; gap: 12px; align-items: center; margin-left: 10px; }
|
|
26
|
+
.serverchan-toggle { width: 34px; height: 34px; padding: 0; border: 1px solid rgba(255,255,255,0.1); border-radius: 50%; background: rgba(255,255,255,0.05); color: var(--text-dim); cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
27
|
+
.serverchan-toggle.active { color: #34c759; background: rgba(52,199,89,0.13); }
|
|
28
|
+
.serverchan-toggle:active { color: #fff; background: rgba(255,255,255,0.12); }
|
|
23
29
|
.btn-join { background: rgba(255,255,255,0.1); border: none; color: var(--primary); padding: 8px 14px; border-radius: 18px; font-weight: 600; font-size: 14px; cursor: pointer; }
|
|
24
30
|
.icon-btn { color: var(--text-dim); background: none; border: none; padding: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
|
25
31
|
.icon-btn:active { color: var(--text); }
|
|
26
32
|
.btn-delete { color: #ff3b30; }
|
|
27
33
|
#modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 10000; display: none; align-items: center; justify-content: center; padding: 20px; }
|
|
28
34
|
#tool-modal { background: var(--card-bg); width: 100%; max-width: 400px; border-radius: 16px; padding: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.4); }
|
|
35
|
+
#settings-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 10020; display: none; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box; }
|
|
36
|
+
#settings-modal { background: var(--card-bg); width: 100%; max-width: 450px; border-radius: 16px; padding: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.4); box-sizing: border-box; }
|
|
37
|
+
.settings-modal-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 20px; }
|
|
38
|
+
.settings-modal-header h2 { margin: 0; font-size: 20px; }
|
|
39
|
+
.settings-modal-header p { margin: 5px 0 0; color: var(--text-dim); font-size: 12px; line-height: 1.45; }
|
|
40
|
+
.settings-close { font-size: 25px; line-height: 1; }
|
|
41
|
+
.settings-section { padding-top: 14px; border-top: 1px solid rgba(255,255,255,0.09); }
|
|
42
|
+
.settings-section-label { margin-bottom: 12px; color: var(--text-dim); font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
|
|
43
|
+
.settings-provider-header { margin-bottom: 16px; }
|
|
44
|
+
.settings-provider-header h3 { margin: 0; font-size: 16px; }
|
|
45
|
+
.settings-provider-header p { margin: 5px 0 0; color: var(--text-dim); font-size: 12px; line-height: 1.45; }
|
|
46
|
+
#settings-modal .form-field { margin-bottom: 14px; }
|
|
47
|
+
#settings-modal .form-field label { display: block; margin-bottom: 7px; color: #f5f5f7; font-size: 13px; font-weight: 650; }
|
|
48
|
+
#settings-modal input, #settings-modal select { width: 100%; min-height: 42px; padding: 9px 10px; box-sizing: border-box; border: 1px solid rgba(255,255,255,0.12); border-radius: 9px; background: rgba(255,255,255,0.08); color: #fff; font-size: 14px; outline: none; }
|
|
49
|
+
#settings-modal input:focus, #settings-modal select:focus { border-color: rgba(0,122,255,0.6); }
|
|
50
|
+
.serverchan-help { margin-top: 6px; color: var(--text-dim); font-size: 11px; line-height: 1.4; }
|
|
51
|
+
.serverchan-settings-status { min-height: 20px; margin: 4px 0 10px; color: var(--text-dim); font-size: 12px; }
|
|
52
|
+
.serverchan-settings-status.success { color: #34c759; }
|
|
53
|
+
.serverchan-settings-status.error { color: #ff6b61; }
|
|
54
|
+
.serverchan-modal-actions { display: flex; align-items: center; gap: 8px; }
|
|
55
|
+
.serverchan-action-spacer { flex: 1; }
|
|
56
|
+
#serverchan-remove-btn { display: none; }
|
|
57
|
+
#app-toast { position: fixed; left: 50%; bottom: max(24px, env(safe-area-inset-bottom)); z-index: 10100; max-width: calc(100vw - 32px); transform: translate(-50%, 18px); opacity: 0; pointer-events: none; padding: 9px 14px; border-radius: 999px; background: rgba(44,44,46,0.96); border: 1px solid rgba(255,255,255,0.12); color: #fff; font-size: 13px; box-shadow: 0 10px 28px rgba(0,0,0,0.35); transition: opacity .18s ease, transform .18s ease; }
|
|
58
|
+
#app-toast.visible { opacity: 1; transform: translate(-50%, 0); }
|
|
29
59
|
.tool-item { padding: 12px; border-bottom: 1px solid #333; cursor: pointer; display: flex; align-items: center; border-radius: 8px; margin-top: 4px; }
|
|
30
60
|
.tool-item:hover { background: rgba(255,255,255,0.05); }
|
|
31
61
|
.tool-icon { width: 32px; height: 32px; background: #333; border-radius: 8px; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
|
|
@@ -85,6 +115,7 @@
|
|
|
85
115
|
#claude-chat-container { display: none; flex: 1; min-height: 0; overflow-y: auto; background: #050505; padding: 12px 12px 24px 12px; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
|
|
86
116
|
.claude-conversation { width: min(100%, var(--chat-content-max)); min-height: 100%; margin: 0 auto; }
|
|
87
117
|
#codex-chat-container { display: none; flex: 1; min-height: 0; overflow-y: auto; background: #050505; padding: 12px 12px 24px 12px; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
|
|
118
|
+
.codex-lazy-detail { padding: 10px 12px; color: var(--text-dim); font-size: 12px; }
|
|
88
119
|
.codex-conversation { width: min(100%, var(--chat-content-max)); margin: 0 auto; }
|
|
89
120
|
.codex-working-indicator, .claude-working-indicator { position: sticky; top: 0; z-index: 4; width: 28px; height: 28px; margin: 0 0 -28px auto; border: 1px solid rgba(255,255,255,.1); border-radius: 50%; background: rgba(28,28,30,.68); box-shadow: 0 5px 16px rgba(0,0,0,.24); backdrop-filter: blur(8px); pointer-events: none; }
|
|
90
121
|
.codex-working-indicator::after, .claude-working-indicator::after { content: ''; position: absolute; inset: 8px; border: 1.5px solid rgba(255,255,255,.7); border-top-color: transparent; border-radius: 50%; animation: codex-spin .8s linear infinite; }
|
|
@@ -379,6 +410,7 @@
|
|
|
379
410
|
.weekday-btn { border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.05); color: var(--text-dim); border-radius: 8px; padding: 8px 0; font-size: 12px; font-weight: 700; cursor: pointer; }
|
|
380
411
|
.weekday-btn.active { border-color: var(--primary); background: rgba(0,122,255,0.25); color: #fff; }
|
|
381
412
|
#schedule-modal { background: var(--card-bg); width: 100%; max-width: 720px; max-height: 92dvh; overflow-y: auto; border-radius: 16px; padding: 18px; box-shadow: 0 20px 40px rgba(0,0,0,0.4); box-sizing: border-box; }
|
|
413
|
+
.schedule-modal-subtitle { margin: 4px 0 0; color: var(--text-dim); font-size: 12px; line-height: 1.4; }
|
|
382
414
|
.step-card { border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 10px; margin-bottom: 8px; background: rgba(255,255,255,0.04); }
|
|
383
415
|
.step-grid { display: grid; grid-template-columns: minmax(110px, 150px) 1fr auto; gap: 8px; align-items: center; }
|
|
384
416
|
@media (max-width: 640px) {
|