@phenx-inc/ctlsurf 0.10.0 → 0.11.0
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/out/headless/index.mjs +1 -1
- package/out/headless/index.mjs.map +1 -1
- package/out/main/index.js +46 -21
- package/out/renderer/assets/{cssMode-BbCVOTaJ.js → cssMode-Bmzy9hKX.js} +3 -3
- package/out/renderer/assets/{freemarker2-DryLywQv.js → freemarker2-CDAsExTa.js} +1 -1
- package/out/renderer/assets/{handlebars-CdUMtSdr.js → handlebars-CvpBYG1L.js} +1 -1
- package/out/renderer/assets/{html-Cwkf2GMH.js → html-CNhEQqr8.js} +1 -1
- package/out/renderer/assets/{htmlMode-C7PzhXxH.js → htmlMode-DD7YPgga.js} +3 -3
- package/out/renderer/assets/{index-Dg1vMspf.js → index-BDlCPzCG.js} +144 -144
- package/out/renderer/assets/{index-BIceGcz3.css → index-CGHeDwZa.css} +5 -46
- package/out/renderer/assets/{javascript-Cy7ROXJl.js → javascript-CsGLsswK.js} +2 -2
- package/out/renderer/assets/{jsonMode-BtZtt3QW.js → jsonMode-CTuGgRvC.js} +3 -3
- package/out/renderer/assets/{liquid-xReAQeYs.js → liquid-BrrN4uul.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Dmx0L0Bw.js → lspLanguageFeatures-Bhdn2Eqy.js} +1 -1
- package/out/renderer/assets/{mdx-YMNFO1Zz.js → mdx-DJo-gax4.js} +1 -1
- package/out/renderer/assets/{python-BExaDVxv.js → python-DYHeg5b2.js} +1 -1
- package/out/renderer/assets/{razor-HL8ANmQ4.js → razor-Cqcks14K.js} +1 -1
- package/out/renderer/assets/{tsMode-Dqd8vdLE.js → tsMode-YTLgCayh.js} +1 -1
- package/out/renderer/assets/{typescript-Dysz1PTX.js → typescript-oxBn_Vcw.js} +1 -1
- package/out/renderer/assets/{xml-BHp5Z9nc.js → xml-BEqGMCTc.js} +1 -1
- package/out/renderer/assets/{yaml-CPDHxVup.js → yaml-BGSrk5HC.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/index.ts +60 -31
- package/src/renderer/App.tsx +9 -7
- package/src/renderer/components/TicketPanel.tsx +20 -25
- package/src/renderer/styles.css +5 -46
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-
|
|
1
|
+
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-BDlCPzCG.js";
|
|
2
2
|
class WorkerManager {
|
|
3
3
|
constructor(_modeId, _defaults) {
|
|
4
4
|
this._modeId = _modeId;
|
package/out/renderer/index.html
CHANGED
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
color: #565f89;
|
|
50
50
|
}
|
|
51
51
|
</style>
|
|
52
|
-
<script type="module" crossorigin src="./assets/index-
|
|
53
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
52
|
+
<script type="module" crossorigin src="./assets/index-BDlCPzCG.js"></script>
|
|
53
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CGHeDwZa.css">
|
|
54
54
|
</head>
|
|
55
55
|
<body>
|
|
56
56
|
<div id="boot-splash">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phenx-inc/ctlsurf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
|
|
5
5
|
"main": "out/main/index.js",
|
|
6
6
|
"bin": {
|
package/src/main/index.ts
CHANGED
|
@@ -51,12 +51,12 @@ function getProjectAbbrev(cwdPath: string): string {
|
|
|
51
51
|
return word.slice(0, 2).toUpperCase()
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// Draw a 32x32 filled circle overlay. `
|
|
55
|
-
// blue and the "
|
|
56
|
-
function createDotOverlay(
|
|
54
|
+
// Draw a 32x32 filled circle overlay (Windows taskbar). `ready` switches between
|
|
55
|
+
// the project-badge blue and the green "ready for you" dot.
|
|
56
|
+
function createDotOverlay(ready: boolean): Electron.NativeImage {
|
|
57
57
|
const size = 32
|
|
58
58
|
const canvas = Buffer.alloc(size * size * 4, 0) // RGBA
|
|
59
|
-
const [r, g, b] =
|
|
59
|
+
const [r, g, b] = ready ? [52, 199, 89] : [122, 162, 247]
|
|
60
60
|
for (let y = 0; y < size; y++) {
|
|
61
61
|
for (let x = 0; x < size; x++) {
|
|
62
62
|
const cx = x - size / 2
|
|
@@ -73,6 +73,43 @@ function createDotOverlay(red: boolean): Electron.NativeImage {
|
|
|
73
73
|
return nativeImage.createFromBuffer(canvas, { width: size, height: size })
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// The plain dock icon (macOS), loaded once at startup. Reused to composite the
|
|
77
|
+
// green "ready" dot on top, and to restore the clean icon when it clears.
|
|
78
|
+
let baseDockIcon: Electron.NativeImage | null = null
|
|
79
|
+
|
|
80
|
+
// Composite a green "ready for you" dot (with a soft dark ring for contrast) into
|
|
81
|
+
// the lower-right of the dock icon. Reads/writes the bitmap as a matched
|
|
82
|
+
// toBitmap()/createFromBitmap() pair, which share the same (BGRA) byte order.
|
|
83
|
+
function dockIconWithReadyDot(base: Electron.NativeImage): Electron.NativeImage {
|
|
84
|
+
const { width, height } = base.getSize()
|
|
85
|
+
if (!width || !height) return base
|
|
86
|
+
const bmp = base.toBitmap()
|
|
87
|
+
const r = Math.round(Math.min(width, height) * 0.17) // dot radius
|
|
88
|
+
const ring = Math.max(2, Math.round(r * 0.22)) // ring thickness
|
|
89
|
+
const cx = width - r - Math.round(width * 0.05)
|
|
90
|
+
const cy = height - r - Math.round(height * 0.05)
|
|
91
|
+
const put = (x: number, y: number, b: number, g: number, rr: number) => {
|
|
92
|
+
const idx = (y * width + x) * 4
|
|
93
|
+
bmp[idx] = b; bmp[idx + 1] = g; bmp[idx + 2] = rr; bmp[idx + 3] = 255
|
|
94
|
+
}
|
|
95
|
+
for (let y = cy - r - ring; y <= cy + r + ring; y++) {
|
|
96
|
+
if (y < 0 || y >= height) continue
|
|
97
|
+
for (let x = cx - r - ring; x <= cx + r + ring; x++) {
|
|
98
|
+
if (x < 0 || x >= width) continue
|
|
99
|
+
const d = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2)
|
|
100
|
+
if (d <= r) put(x, y, 89, 199, 52) // green #34c759 (BGRA)
|
|
101
|
+
else if (d <= r + ring) put(x, y, 24, 26, 31) // dark ring #1f1a18
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return nativeImage.createFromBitmap(bmp, { width, height })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Show/clear the green "ready" dot on the macOS dock icon.
|
|
108
|
+
function setDockReadyDot(on: boolean): void {
|
|
109
|
+
if (process.platform !== 'darwin' || !app.dock || !baseDockIcon) return
|
|
110
|
+
app.dock.setIcon(on ? dockIconWithReadyDot(baseDockIcon) : baseDockIcon)
|
|
111
|
+
}
|
|
112
|
+
|
|
76
113
|
// Last project abbreviation, so the steady badge can be restored after the
|
|
77
114
|
// "awaiting input" indicator clears.
|
|
78
115
|
let projectAbbrev = ''
|
|
@@ -90,8 +127,9 @@ function updateProjectBadge(cwdPath: string | null): void {
|
|
|
90
127
|
if (!cwdPath) return
|
|
91
128
|
projectAbbrev = getProjectAbbrev(cwdPath)
|
|
92
129
|
|
|
93
|
-
//
|
|
94
|
-
|
|
130
|
+
// The project badge and the "ready" dot are now independent surfaces (badge
|
|
131
|
+
// vs dock icon), so the badge can always update.
|
|
132
|
+
applySteadyBadge()
|
|
95
133
|
|
|
96
134
|
// All platforms: update window title
|
|
97
135
|
if (mainWindow) {
|
|
@@ -100,18 +138,17 @@ function updateProjectBadge(cwdPath: string | null): void {
|
|
|
100
138
|
}
|
|
101
139
|
}
|
|
102
140
|
|
|
103
|
-
// ─── "
|
|
141
|
+
// ─── "Ready for you" attention indicator (cross-platform) ──────────
|
|
104
142
|
//
|
|
105
143
|
// The detection is OS-independent; only the visual differs:
|
|
106
|
-
// macOS —
|
|
107
|
-
// Windows —
|
|
144
|
+
// macOS — steady green dot composited on the dock icon + one bounce
|
|
145
|
+
// Windows — green overlay icon on the taskbar button + flash the button
|
|
108
146
|
// Linux — badge count where supported + flash (window urgency hint)
|
|
109
147
|
// While the window is focused the dock/taskbar nag is suppressed (the in-app
|
|
110
148
|
// dot is enough); the renderer always gets `agent:state` regardless of OS.
|
|
111
149
|
|
|
112
150
|
const attention = new AttentionController()
|
|
113
|
-
let
|
|
114
|
-
let blinkOn = false
|
|
151
|
+
let attentionActive = false
|
|
115
152
|
let bounceId = -1
|
|
116
153
|
|
|
117
154
|
function setAwaitingIndicator(on: boolean): void {
|
|
@@ -124,40 +161,31 @@ function applyAttention(active: boolean): void {
|
|
|
124
161
|
}
|
|
125
162
|
|
|
126
163
|
function startAttention(): void {
|
|
127
|
-
if (
|
|
128
|
-
|
|
164
|
+
if (attentionActive) return
|
|
165
|
+
attentionActive = true
|
|
166
|
+
log('[agent-state] attention START (green ready dot + bounce)')
|
|
129
167
|
|
|
130
168
|
if (process.platform === 'darwin' && app.dock) {
|
|
169
|
+
setDockReadyDot(true)
|
|
131
170
|
bounceId = app.dock.bounce('informational')
|
|
132
171
|
}
|
|
133
172
|
if (process.platform === 'win32' && mainWindow) {
|
|
134
|
-
mainWindow.setOverlayIcon(createDotOverlay(true), '
|
|
173
|
+
mainWindow.setOverlayIcon(createDotOverlay(true), 'Ready for you')
|
|
135
174
|
mainWindow.flashFrame(true)
|
|
136
175
|
}
|
|
137
176
|
if (process.platform === 'linux') {
|
|
138
177
|
if (typeof app.badgeCount === 'number') app.badgeCount = 1
|
|
139
178
|
mainWindow?.flashFrame(true)
|
|
140
179
|
}
|
|
141
|
-
|
|
142
|
-
// Blink the macOS dock badge — the whole red pill (project initials and all)
|
|
143
|
-
// toggles on/off, so the project letters are preserved on the "on" frames
|
|
144
|
-
// rather than being replaced by a dot.
|
|
145
|
-
blinkOn = true
|
|
146
|
-
blinkTimer = setInterval(() => {
|
|
147
|
-
blinkOn = !blinkOn
|
|
148
|
-
if (process.platform === 'darwin' && app.dock) {
|
|
149
|
-
app.dock.setBadge(blinkOn ? (projectAbbrev || '●') : '')
|
|
150
|
-
}
|
|
151
|
-
}, 600)
|
|
152
180
|
}
|
|
153
181
|
|
|
154
182
|
function stopAttention(): void {
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
183
|
+
if (!attentionActive) return
|
|
184
|
+
attentionActive = false
|
|
185
|
+
log('[agent-state] attention STOP')
|
|
186
|
+
|
|
160
187
|
if (process.platform === 'darwin' && app.dock) {
|
|
188
|
+
setDockReadyDot(false)
|
|
161
189
|
if (bounceId >= 0) { app.dock.cancelBounce(bounceId); bounceId = -1 }
|
|
162
190
|
}
|
|
163
191
|
if (process.platform === 'win32' && mainWindow) {
|
|
@@ -582,7 +610,8 @@ app.whenReady().then(() => {
|
|
|
582
610
|
if (process.platform === 'darwin' && app.dock) {
|
|
583
611
|
const iconPath = path.join(__dirname, '../../resources/icon.png')
|
|
584
612
|
try {
|
|
585
|
-
|
|
613
|
+
baseDockIcon = nativeImage.createFromPath(iconPath)
|
|
614
|
+
app.dock.setIcon(baseDockIcon)
|
|
586
615
|
} catch { /* ignore */ }
|
|
587
616
|
}
|
|
588
617
|
|
package/src/renderer/App.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from './components/PaneLayout'
|
|
15
15
|
import { StatusBar } from './components/StatusBar'
|
|
16
16
|
import { SettingsDialog } from './components/SettingsDialog'
|
|
17
|
-
import {
|
|
17
|
+
import { TicketPane } from './components/TicketPanel'
|
|
18
18
|
|
|
19
19
|
interface AgentConfig {
|
|
20
20
|
id: string
|
|
@@ -96,11 +96,13 @@ const DEFAULT_LAYOUT: LayoutNode = {
|
|
|
96
96
|
direction: 'horizontal',
|
|
97
97
|
children: [
|
|
98
98
|
{ type: 'leaf', paneId: 'terminal' },
|
|
99
|
-
{ type: 'leaf', paneId: '
|
|
99
|
+
{ type: 'leaf', paneId: 'tickets' },
|
|
100
100
|
],
|
|
101
101
|
sizes: [50, 50],
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// Panes shown as plain toggle buttons in the titlebar. `tickets` has its own
|
|
105
|
+
// dedicated icon button, so it's not listed here.
|
|
104
106
|
const ALL_PANE_IDS = ['editor', 'terminal', 'ctlsurf']
|
|
105
107
|
|
|
106
108
|
interface TabInfo {
|
|
@@ -141,7 +143,6 @@ export default function App() {
|
|
|
141
143
|
// Per-tab agent activity ('busy' | 'awaiting_input' | 'awaiting_decision' | 'idle'),
|
|
142
144
|
// driven by the main process. Powers the awaiting-input dot on each tab.
|
|
143
145
|
const [agentStates, setAgentStates] = useState<Record<string, string>>({})
|
|
144
|
-
const [showTicketPanel, setShowTicketPanel] = useState(false)
|
|
145
146
|
// Draggable on-canvas push-to-talk mic; visibility persists across launches.
|
|
146
147
|
const [showFloatingMic, setShowFloatingMic] = useState<boolean>(() => {
|
|
147
148
|
try { return localStorage.getItem('ctlsurf.floatingMicVisible') !== 'false' } catch { return true }
|
|
@@ -399,6 +400,7 @@ export default function App() {
|
|
|
399
400
|
case 'E': e.preventDefault(); togglePane('editor'); break
|
|
400
401
|
case 'T': e.preventDefault(); togglePane('terminal'); break
|
|
401
402
|
case 'P': e.preventDefault(); togglePane('ctlsurf'); break
|
|
403
|
+
case 'K': e.preventDefault(); togglePane('tickets'); break
|
|
402
404
|
}
|
|
403
405
|
}
|
|
404
406
|
}
|
|
@@ -488,6 +490,7 @@ export default function App() {
|
|
|
488
490
|
),
|
|
489
491
|
},
|
|
490
492
|
{ id: 'ctlsurf', label: 'ctlsurf', content: <CtlsurfPanel navigate={ctlsurfNav} /> },
|
|
493
|
+
{ id: 'tickets', label: 'Tickets', content: <TicketPane /> },
|
|
491
494
|
]
|
|
492
495
|
|
|
493
496
|
return (
|
|
@@ -516,9 +519,9 @@ export default function App() {
|
|
|
516
519
|
<span className={`tracking-dot ${trackingActive ? 'on' : 'off'}`} />
|
|
517
520
|
</button>
|
|
518
521
|
<button
|
|
519
|
-
className={`titlebar-btn titlebar-icon-btn ${
|
|
520
|
-
onClick={() =>
|
|
521
|
-
title="
|
|
522
|
+
className={`titlebar-btn titlebar-icon-btn ${visiblePaneIds.includes('tickets') ? 'active' : ''}`}
|
|
523
|
+
onClick={() => togglePane('tickets')}
|
|
524
|
+
title="Toggle tickets pane for the active project"
|
|
522
525
|
aria-label="Tickets"
|
|
523
526
|
>
|
|
524
527
|
<svg className="ticket-tag-icon" viewBox="0 0 24 24" width="13" height="13"
|
|
@@ -589,7 +592,6 @@ export default function App() {
|
|
|
589
592
|
|
|
590
593
|
<StatusBar wsStatus={wsStatus} cwd={cwd} onChangeCwd={handleChangeCwd} updateInfo={updateInfo} />
|
|
591
594
|
<SettingsDialog open={showSettings} onClose={() => setShowSettings(false)} />
|
|
592
|
-
<TicketPanel open={showTicketPanel} onClose={() => setShowTicketPanel(false)} />
|
|
593
595
|
|
|
594
596
|
{showAgentPicker && agents.length > 0 && (
|
|
595
597
|
<AgentPicker
|
|
@@ -26,11 +26,6 @@ interface Ticket {
|
|
|
26
26
|
created: string | null
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
interface TicketPanelProps {
|
|
30
|
-
open: boolean
|
|
31
|
-
onClose: () => void
|
|
32
|
-
}
|
|
33
|
-
|
|
34
29
|
type SaveState = { kind: 'idle' } | { kind: 'saving' } | { kind: 'error'; message: string }
|
|
35
30
|
type View = 'list' | 'form'
|
|
36
31
|
|
|
@@ -79,7 +74,10 @@ function Segmented({ options, value, onChange }: SegmentedProps) {
|
|
|
79
74
|
)
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
|
|
77
|
+
// Docked side-pane variant of the ticket UI: header + list/form body + footer.
|
|
78
|
+
// It only mounts when its pane is visible, so it loads on mount and refreshes
|
|
79
|
+
// when the connected project (cwd) changes. Closed via the pane's own header.
|
|
80
|
+
export function TicketPane() {
|
|
83
81
|
const [project, setProject] = useState<string | null>(null)
|
|
84
82
|
const [view, setView] = useState<View>('list')
|
|
85
83
|
|
|
@@ -111,17 +109,18 @@ export function TicketPanel({ open, onClose }: TicketPanelProps) {
|
|
|
111
109
|
}
|
|
112
110
|
}, [])
|
|
113
111
|
|
|
114
|
-
//
|
|
112
|
+
// Load project + tickets on mount, and refresh whenever the connected project
|
|
113
|
+
// (cwd) changes so the list always tracks the active project.
|
|
115
114
|
useEffect(() => {
|
|
116
|
-
if (!open) return
|
|
117
|
-
setView('list')
|
|
118
115
|
let cancelled = false
|
|
119
|
-
window.worker.getTicketProject()
|
|
116
|
+
const loadProject = () => window.worker.getTicketProject()
|
|
120
117
|
.then(r => { if (!cancelled) setProject(r?.name ?? null) })
|
|
121
118
|
.catch(() => { if (!cancelled) setProject(null) })
|
|
119
|
+
loadProject()
|
|
122
120
|
void refreshList()
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
const unsub = window.worker.onCwdChanged(() => { loadProject(); void refreshList() })
|
|
122
|
+
return () => { cancelled = true; unsub() }
|
|
123
|
+
}, [refreshList])
|
|
125
124
|
|
|
126
125
|
const openNewForm = useCallback(() => {
|
|
127
126
|
setEditingId(null)
|
|
@@ -162,28 +161,26 @@ export function TicketPanel({ open, onClose }: TicketPanelProps) {
|
|
|
162
161
|
}
|
|
163
162
|
}, [title, description, status, priority, editingId, save.kind, refreshList])
|
|
164
163
|
|
|
165
|
-
//
|
|
164
|
+
// Only capture keys while editing a ticket, so list-view typing in the terminal
|
|
165
|
+
// isn't hijacked. Esc returns to the list; Cmd/Ctrl+Enter saves.
|
|
166
166
|
useEffect(() => {
|
|
167
|
-
if (
|
|
167
|
+
if (view !== 'form') return
|
|
168
168
|
const onKey = (e: KeyboardEvent) => {
|
|
169
169
|
if (e.key === 'Escape') {
|
|
170
170
|
e.preventDefault()
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && view === 'form') {
|
|
171
|
+
setView('list')
|
|
172
|
+
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
174
173
|
e.preventDefault()
|
|
175
174
|
void handleSave()
|
|
176
175
|
}
|
|
177
176
|
}
|
|
178
177
|
window.addEventListener('keydown', onKey)
|
|
179
178
|
return () => window.removeEventListener('keydown', onKey)
|
|
180
|
-
}, [
|
|
179
|
+
}, [view, handleSave])
|
|
181
180
|
|
|
182
181
|
return (
|
|
183
|
-
|
|
184
|
-
<div className=
|
|
185
|
-
<div className={`ticket-drawer ${open ? 'open' : ''}`} role="dialog" aria-label="Tickets">
|
|
186
|
-
<div className="ticket-drawer-header">
|
|
182
|
+
<div className="ticket-pane" role="region" aria-label="Tickets">
|
|
183
|
+
<div className="ticket-drawer-header">
|
|
187
184
|
<div className="ticket-drawer-heading">
|
|
188
185
|
{view === 'form' ? (
|
|
189
186
|
<button className="ticket-drawer-back" onClick={() => setView('list')} title="Back (Esc)">‹</button>
|
|
@@ -201,7 +198,6 @@ export function TicketPanel({ open, onClose }: TicketPanelProps) {
|
|
|
201
198
|
</span>
|
|
202
199
|
</div>
|
|
203
200
|
</div>
|
|
204
|
-
<button className="ticket-drawer-close" onClick={onClose} title="Close">×</button>
|
|
205
201
|
</div>
|
|
206
202
|
|
|
207
203
|
{view === 'list' ? (
|
|
@@ -302,7 +298,6 @@ export function TicketPanel({ open, onClose }: TicketPanelProps) {
|
|
|
302
298
|
</div>
|
|
303
299
|
</>
|
|
304
300
|
)}
|
|
305
|
-
|
|
306
|
-
</>
|
|
301
|
+
</div>
|
|
307
302
|
)
|
|
308
303
|
}
|
package/src/renderer/styles.css
CHANGED
|
@@ -1458,38 +1458,13 @@ html, body, #root {
|
|
|
1458
1458
|
|
|
1459
1459
|
/* ─── Ticket drawer ──────────────────────────────── */
|
|
1460
1460
|
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
inset: 0;
|
|
1464
|
-
background: rgba(13, 14, 22, 0.55);
|
|
1465
|
-
backdrop-filter: blur(2px);
|
|
1466
|
-
opacity: 0;
|
|
1467
|
-
pointer-events: none;
|
|
1468
|
-
transition: opacity 0.2s ease;
|
|
1469
|
-
z-index: 90;
|
|
1470
|
-
}
|
|
1471
|
-
.ticket-scrim.open {
|
|
1472
|
-
opacity: 1;
|
|
1473
|
-
pointer-events: auto;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
.ticket-drawer {
|
|
1477
|
-
position: fixed;
|
|
1478
|
-
top: 0;
|
|
1479
|
-
right: 0;
|
|
1480
|
-
bottom: 0;
|
|
1481
|
-
width: 380px;
|
|
1482
|
-
background: linear-gradient(180deg, #20243a 0%, #1b1e2e 100%);
|
|
1483
|
-
border-left: 1px solid #343860;
|
|
1484
|
-
box-shadow: -16px 0 48px rgba(0, 0, 0, 0.5);
|
|
1461
|
+
/* Docked side-pane variant: fills its pane (header/body/footer flex column). */
|
|
1462
|
+
.ticket-pane {
|
|
1485
1463
|
display: flex;
|
|
1486
1464
|
flex-direction: column;
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1491
|
-
.ticket-drawer.open {
|
|
1492
|
-
transform: translateX(0);
|
|
1465
|
+
height: 100%;
|
|
1466
|
+
min-height: 0;
|
|
1467
|
+
background: linear-gradient(180deg, #20243a 0%, #1b1e2e 100%);
|
|
1493
1468
|
}
|
|
1494
1469
|
|
|
1495
1470
|
/* ── header ── */
|
|
@@ -1539,22 +1514,6 @@ html, body, #root {
|
|
|
1539
1514
|
.ticket-drawer-warn {
|
|
1540
1515
|
color: #e0af68;
|
|
1541
1516
|
}
|
|
1542
|
-
.ticket-drawer-close {
|
|
1543
|
-
background: none;
|
|
1544
|
-
border: none;
|
|
1545
|
-
color: #565f89;
|
|
1546
|
-
font-size: 20px;
|
|
1547
|
-
line-height: 1;
|
|
1548
|
-
cursor: pointer;
|
|
1549
|
-
padding: 2px 6px;
|
|
1550
|
-
border-radius: 6px;
|
|
1551
|
-
transition: background 0.12s ease, color 0.12s ease;
|
|
1552
|
-
}
|
|
1553
|
-
.ticket-drawer-close:hover {
|
|
1554
|
-
color: #d5dbf5;
|
|
1555
|
-
background: rgba(255, 255, 255, 0.06);
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
1517
|
/* ── body ── */
|
|
1559
1518
|
.ticket-drawer-body {
|
|
1560
1519
|
flex: 1;
|