command_deck 0.1.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.
- checksums.yaml +7 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +17 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +141 -0
- data/Rakefile +12 -0
- data/TAGS +521 -0
- data/app/controllers/command_deck/actions_controller.rb +18 -0
- data/app/controllers/command_deck/assets_controller.rb +48 -0
- data/app/controllers/command_deck/base_controller.rb +17 -0
- data/app/controllers/command_deck/panels_controller.rb +108 -0
- data/config/routes.rb +9 -0
- data/lib/command_deck/assets/css/main.css +393 -0
- data/lib/command_deck/assets/js/core/api.js +20 -0
- data/lib/command_deck/assets/js/core/dom.js +33 -0
- data/lib/command_deck/assets/js/core/store.js +13 -0
- data/lib/command_deck/assets/js/main.js +39 -0
- data/lib/command_deck/assets/js/ui/overlay.js +305 -0
- data/lib/command_deck/assets/js/ui/panel_selector.js +32 -0
- data/lib/command_deck/assets/js/ui/position_manager.js +26 -0
- data/lib/command_deck/assets/js/ui/settings_dropdown.js +64 -0
- data/lib/command_deck/assets/js/ui/theme_manager.js +34 -0
- data/lib/command_deck/engine.rb +28 -0
- data/lib/command_deck/executor.rb +70 -0
- data/lib/command_deck/injector.rb +28 -0
- data/lib/command_deck/middleware.rb +57 -0
- data/lib/command_deck/railtie.rb +13 -0
- data/lib/command_deck/registry.rb +103 -0
- data/lib/command_deck/version.rb +5 -0
- data/lib/command_deck.rb +14 -0
- data/public/img/demo.png +0 -0
- data/sig/command_deck.rbs +4 -0
- metadata +110 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandDeck
|
4
|
+
# Controller for serving panels
|
5
|
+
class PanelsController < BaseController
|
6
|
+
def index
|
7
|
+
render json: { panels: serialize_panels }
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def serialize_panels
|
13
|
+
Registry.panels.map { |panel| serialize_panel(panel) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def serialize_panel(panel)
|
17
|
+
{
|
18
|
+
key: panel.key,
|
19
|
+
title: panel.title,
|
20
|
+
owner: panel.owner,
|
21
|
+
group: panel.group,
|
22
|
+
tabs: serialize_tabs(panel.tabs)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def serialize_tabs(tabs)
|
27
|
+
tabs.map { |tab| serialize_tab(tab) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def serialize_tab(tab)
|
31
|
+
{ title: tab.title, actions: serialize_actions(tab.actions) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def serialize_actions(actions)
|
35
|
+
actions.map { |action| serialize_action(action) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def serialize_action(action)
|
39
|
+
{ title: action.title, key: action.key, params: serialize_params(action.params) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def serialize_params(params)
|
43
|
+
(params || []).map { |p| serialize_param(p) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def serialize_param(param)
|
47
|
+
base = base_param(param)
|
48
|
+
|
49
|
+
choices = extract_choices(param)
|
50
|
+
if choices
|
51
|
+
base[:choices] = normalize_choices(choices)
|
52
|
+
base[:include_blank] = param[:include_blank] if param.key?(:include_blank)
|
53
|
+
end
|
54
|
+
|
55
|
+
base[:return] = param[:return] if param.key?(:return)
|
56
|
+
base
|
57
|
+
end
|
58
|
+
|
59
|
+
def normalize_choices(choices)
|
60
|
+
arrayify(choices).map { |item| normalize_choice_item(item) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def base_param(param)
|
64
|
+
{
|
65
|
+
name: param[:name],
|
66
|
+
type: param[:type].to_s,
|
67
|
+
label: param[:label] || param[:name].to_s.tr("_", " ").capitalize,
|
68
|
+
required: param.key?(:required) ? param[:required] : false
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def extract_choices(param)
|
73
|
+
return param[:options] if param.key?(:options)
|
74
|
+
|
75
|
+
collection = param[:collection]
|
76
|
+
collection.respond_to?(:call) ? collection.call : nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def arrayify(obj)
|
80
|
+
obj.is_a?(Array) ? obj : obj.to_a
|
81
|
+
end
|
82
|
+
|
83
|
+
def normalize_choice_item(item)
|
84
|
+
return normalize_choice_hash(item) if item.is_a?(Hash)
|
85
|
+
return normalize_choice_array(item) if item.is_a?(Array)
|
86
|
+
|
87
|
+
normalize_choice_default(item)
|
88
|
+
end
|
89
|
+
|
90
|
+
def normalize_choice_hash(item)
|
91
|
+
out = {}
|
92
|
+
out[:label] = item[:label].to_s if item.key?(:label)
|
93
|
+
out[:value] = item[:value] if item.key?(:value)
|
94
|
+
out[:meta] = item[:meta] if item.key?(:meta)
|
95
|
+
out
|
96
|
+
end
|
97
|
+
|
98
|
+
def normalize_choice_array(item)
|
99
|
+
return normalize_choice_default(item) if item.size < 2
|
100
|
+
|
101
|
+
{ label: item[0].to_s, value: item[1] }
|
102
|
+
end
|
103
|
+
|
104
|
+
def normalize_choice_default(item)
|
105
|
+
{ label: item.to_s, value: item }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,393 @@
|
|
1
|
+
/* ==========================================
|
2
|
+
Command Deck • main.css (scoped)
|
3
|
+
========================================== */
|
4
|
+
|
5
|
+
/* ---------- Base & Panel ---------- */
|
6
|
+
#command-deck-toggle {
|
7
|
+
position: fixed;
|
8
|
+
z-index: 2147483000;
|
9
|
+
right: 16px;
|
10
|
+
bottom: 16px;
|
11
|
+
width: 44px;
|
12
|
+
height: 44px;
|
13
|
+
border-radius: 22px;
|
14
|
+
background: #111;
|
15
|
+
color: #fff;
|
16
|
+
display: flex;
|
17
|
+
align-items: center;
|
18
|
+
justify-content: center;
|
19
|
+
font: 600 14px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
20
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, .25);
|
21
|
+
cursor: pointer;
|
22
|
+
}
|
23
|
+
|
24
|
+
/* Dark theme for toggle */
|
25
|
+
#command-deck-toggle.cd-theme-dark {
|
26
|
+
background: #000; /* full black */
|
27
|
+
color: #fff;
|
28
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, .6);
|
29
|
+
}
|
30
|
+
|
31
|
+
#command-deck-panel {
|
32
|
+
position: fixed;
|
33
|
+
z-index: 2147483000;
|
34
|
+
right: 16px;
|
35
|
+
bottom: 72px;
|
36
|
+
width: 360px;
|
37
|
+
max-width: 92vw;
|
38
|
+
background: #fff;
|
39
|
+
color: #111;
|
40
|
+
border-radius: 12px;
|
41
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, .28);
|
42
|
+
padding: 12px;
|
43
|
+
border: 1px solid rgba(0, 0, 0, .06);
|
44
|
+
}
|
45
|
+
|
46
|
+
/* Dark theme for panel */
|
47
|
+
#command-deck-panel.cd-theme-dark {
|
48
|
+
background: #000; /* full black */
|
49
|
+
color: #f1f1f1;
|
50
|
+
border-color: #222;
|
51
|
+
box-shadow: 0 12px 40px rgba(0,0,0,.7);
|
52
|
+
}
|
53
|
+
|
54
|
+
#command-deck-panel.cd-theme-dark #cd-tabs-wrap .cd-tab-btn,
|
55
|
+
#command-deck-panel.cd-theme-dark #cd-settings-btn,
|
56
|
+
#command-deck-panel.cd-theme-dark .cd-menu-item,
|
57
|
+
#command-deck-panel.cd-theme-dark .cd-form input[type="text"],
|
58
|
+
#command-deck-panel.cd-theme-dark .cd-form input[type="number"],
|
59
|
+
#command-deck-panel.cd-theme-dark .cd-form select,
|
60
|
+
#command-deck-panel.cd-theme-dark #cd-panel-selector select {
|
61
|
+
background: #111; /* near-black */
|
62
|
+
color: #eee;
|
63
|
+
border-color: #333;
|
64
|
+
}
|
65
|
+
|
66
|
+
#command-deck-panel.cd-theme-dark .cd-action { border-color: #222; }
|
67
|
+
|
68
|
+
/* Dark mode: improve readability of labels and placeholders */
|
69
|
+
#command-deck-panel.cd-theme-dark .cd-form label {
|
70
|
+
color: #cfcfcf;
|
71
|
+
}
|
72
|
+
|
73
|
+
#command-deck-panel.cd-theme-dark .cd-form input[type="text"],
|
74
|
+
#command-deck-panel.cd-theme-dark .cd-form input[type="number"] {
|
75
|
+
caret-color: #fff;
|
76
|
+
}
|
77
|
+
|
78
|
+
#command-deck-panel.cd-theme-dark .cd-form input[type="text"]::placeholder,
|
79
|
+
#command-deck-panel.cd-theme-dark .cd-form input[type="number"]::placeholder {
|
80
|
+
color: #9a9a9a;
|
81
|
+
opacity: 1; /* Ensure consistent placeholder contrast */
|
82
|
+
}
|
83
|
+
|
84
|
+
#command-deck-panel.cd-theme-dark #command-deck-output {
|
85
|
+
background: #0d0d0d;
|
86
|
+
color: #eee;
|
87
|
+
}
|
88
|
+
|
89
|
+
/* Theme toggle button */
|
90
|
+
#cd-theme-toggle {
|
91
|
+
border: 1px solid #ddd;
|
92
|
+
background: #f9f9f9;
|
93
|
+
border-radius: 6px;
|
94
|
+
padding: 4px 8px;
|
95
|
+
cursor: pointer;
|
96
|
+
margin-top: 8px;
|
97
|
+
}
|
98
|
+
|
99
|
+
#command-deck-panel.cd-theme-dark #cd-theme-toggle {
|
100
|
+
background: #111827;
|
101
|
+
color: #e5e7eb;
|
102
|
+
border-color: #374151;
|
103
|
+
}
|
104
|
+
|
105
|
+
#command-deck-panel * {
|
106
|
+
font: 500 13px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
107
|
+
}
|
108
|
+
|
109
|
+
/* ---------- Header & Settings ---------- */
|
110
|
+
#cd-header {
|
111
|
+
display: flex;
|
112
|
+
align-items: center;
|
113
|
+
justify-content: space-between;
|
114
|
+
gap: 8px;
|
115
|
+
position: relative;
|
116
|
+
}
|
117
|
+
|
118
|
+
#command-deck-panel h4 {
|
119
|
+
margin: 0 0 8px;
|
120
|
+
font-size: 14px;
|
121
|
+
}
|
122
|
+
|
123
|
+
#cd-settings-btn {
|
124
|
+
border: 1px solid #ddd;
|
125
|
+
background: #f9f9f9;
|
126
|
+
border-radius: 6px;
|
127
|
+
padding: 4px 8px;
|
128
|
+
cursor: pointer;
|
129
|
+
}
|
130
|
+
|
131
|
+
#cd-settings-btn.open {
|
132
|
+
background: #eef4ff;
|
133
|
+
border-color: #cfe0ff;
|
134
|
+
}
|
135
|
+
|
136
|
+
#cd-header-right {
|
137
|
+
display: flex;
|
138
|
+
align-items: center;
|
139
|
+
gap: 8px;
|
140
|
+
}
|
141
|
+
|
142
|
+
#cd-panel-selector select {
|
143
|
+
padding: 4px 8px;
|
144
|
+
border: 1px solid #ddd;
|
145
|
+
border-radius: 6px;
|
146
|
+
background: #fff;
|
147
|
+
}
|
148
|
+
|
149
|
+
#cd-settings-panel {
|
150
|
+
position: absolute;
|
151
|
+
top: 32px;
|
152
|
+
right: 0;
|
153
|
+
background: #fff;
|
154
|
+
border: 1px solid #e5e5e5;
|
155
|
+
border-radius: 8px;
|
156
|
+
box-shadow: 0 10px 28px rgba(0,0,0,.12);
|
157
|
+
padding: 8px;
|
158
|
+
z-index: 2147483001;
|
159
|
+
min-width: 180px;
|
160
|
+
}
|
161
|
+
|
162
|
+
/* Dark theme for settings dropdown */
|
163
|
+
#command-deck-panel.cd-theme-dark #cd-settings-panel {
|
164
|
+
background: #0d0d0d;
|
165
|
+
border-color: #333;
|
166
|
+
box-shadow: 0 10px 28px rgba(0,0,0,.6);
|
167
|
+
}
|
168
|
+
|
169
|
+
.cd-settings-title {
|
170
|
+
font-weight: 600;
|
171
|
+
margin-bottom: 6px;
|
172
|
+
}
|
173
|
+
|
174
|
+
.cd-menu {
|
175
|
+
display: flex;
|
176
|
+
flex-direction: column;
|
177
|
+
gap: 6px;
|
178
|
+
}
|
179
|
+
|
180
|
+
.cd-menu-item {
|
181
|
+
text-align: left;
|
182
|
+
padding: 6px 8px;
|
183
|
+
border: 1px solid #eee;
|
184
|
+
background: #f9f9f9;
|
185
|
+
border-radius: 6px;
|
186
|
+
cursor: pointer;
|
187
|
+
}
|
188
|
+
|
189
|
+
.cd-menu-item.active {
|
190
|
+
background: #0d6efd;
|
191
|
+
color: #fff;
|
192
|
+
border-color: #0d6efd;
|
193
|
+
}
|
194
|
+
|
195
|
+
/* ---------- Tabs & Actions ---------- */
|
196
|
+
#cd-tabs-wrap {
|
197
|
+
display: flex;
|
198
|
+
gap: 6px;
|
199
|
+
flex-wrap: wrap;
|
200
|
+
margin: 6px 0 8px;
|
201
|
+
}
|
202
|
+
|
203
|
+
#cd-tabs-wrap .cd-tab-btn {
|
204
|
+
padding: 6px 8px;
|
205
|
+
border: 1px solid #ddd;
|
206
|
+
background: #f9f9f9;
|
207
|
+
border-radius: 6px;
|
208
|
+
cursor: pointer;
|
209
|
+
}
|
210
|
+
|
211
|
+
#cd-actions-wrap {
|
212
|
+
display: flex;
|
213
|
+
flex-direction: column;
|
214
|
+
gap: 12px;
|
215
|
+
}
|
216
|
+
|
217
|
+
.cd-action {
|
218
|
+
border: 1px solid #eee;
|
219
|
+
border-radius: 8px;
|
220
|
+
padding: 10px;
|
221
|
+
}
|
222
|
+
|
223
|
+
.cd-action-title {
|
224
|
+
font-weight: 600;
|
225
|
+
margin-bottom: 8px;
|
226
|
+
}
|
227
|
+
|
228
|
+
.cd-form label {
|
229
|
+
display: block;
|
230
|
+
margin: 6px 0 2px;
|
231
|
+
color: #555;
|
232
|
+
}
|
233
|
+
|
234
|
+
.cd-form label .cd-required {
|
235
|
+
color: #d00;
|
236
|
+
margin-left: 4px;
|
237
|
+
font-weight: 700;
|
238
|
+
}
|
239
|
+
|
240
|
+
.cd-form input[type="text"],
|
241
|
+
.cd-form input[type="number"],
|
242
|
+
.cd-form select {
|
243
|
+
width: 100%;
|
244
|
+
box-sizing: border-box;
|
245
|
+
padding: 8px 10px;
|
246
|
+
border: 1px solid #ddd;
|
247
|
+
border-radius: 6px;
|
248
|
+
}
|
249
|
+
|
250
|
+
.cd-actions-row {
|
251
|
+
display: flex;
|
252
|
+
justify-content: flex-end;
|
253
|
+
margin-top: 8px;
|
254
|
+
}
|
255
|
+
|
256
|
+
.cd-param-hint {
|
257
|
+
margin-top: 6px;
|
258
|
+
color: #666;
|
259
|
+
font-size: 12px;
|
260
|
+
}
|
261
|
+
|
262
|
+
.cd-badge {
|
263
|
+
display: inline-block;
|
264
|
+
padding: 2px 6px;
|
265
|
+
border-radius: 999px;
|
266
|
+
font-size: 11px;
|
267
|
+
font-weight: 600;
|
268
|
+
margin-left: 4px;
|
269
|
+
}
|
270
|
+
|
271
|
+
.cd-badge.cd-on {
|
272
|
+
background: #e6f4ea;
|
273
|
+
color: #137333;
|
274
|
+
border: 1px solid #c8e6c9;
|
275
|
+
}
|
276
|
+
|
277
|
+
.cd-badge.cd-off {
|
278
|
+
background: #fde8e8;
|
279
|
+
color: #b91c1c;
|
280
|
+
border: 1px solid #fecaca;
|
281
|
+
}
|
282
|
+
|
283
|
+
#command-deck-panel.cd-theme-dark .cd-param-hint {
|
284
|
+
color: #bdbdbd;
|
285
|
+
}
|
286
|
+
#command-deck-panel.cd-theme-dark .cd-badge.cd-on {
|
287
|
+
background: #0b2f1c;
|
288
|
+
color: #a7f3d0;
|
289
|
+
border-color: #065f46;
|
290
|
+
}
|
291
|
+
#command-deck-panel.cd-theme-dark .cd-badge.cd-off {
|
292
|
+
background: #3b0c0c;
|
293
|
+
color: #fecaca;
|
294
|
+
border-color: #7f1d1d;
|
295
|
+
}
|
296
|
+
|
297
|
+
.cd-form .cd-run {
|
298
|
+
width: 36px;
|
299
|
+
height: 32px;
|
300
|
+
display: inline-flex;
|
301
|
+
align-items: center;
|
302
|
+
justify-content: center;
|
303
|
+
padding: 0;
|
304
|
+
border: 0;
|
305
|
+
border-radius: 6px;
|
306
|
+
background: #0d6efd;
|
307
|
+
color: #fff;
|
308
|
+
cursor: pointer;
|
309
|
+
font-size: 16px;
|
310
|
+
}
|
311
|
+
|
312
|
+
.cd-form .cd-run:hover:not(:disabled) {
|
313
|
+
background: #0b5ed7; /* darker */
|
314
|
+
}
|
315
|
+
|
316
|
+
.cd-form .cd-run:focus-visible {
|
317
|
+
outline: 2px solid #9ec5fe;
|
318
|
+
outline-offset: 2px;
|
319
|
+
}
|
320
|
+
|
321
|
+
.cd-form .cd-run:disabled,
|
322
|
+
.cd-form .cd-run.loading {
|
323
|
+
opacity: 0.6;
|
324
|
+
cursor: not-allowed;
|
325
|
+
}
|
326
|
+
|
327
|
+
#command-deck-output {
|
328
|
+
white-space: pre-wrap;
|
329
|
+
background: #0b102114;
|
330
|
+
color: #0b1d30;
|
331
|
+
border-radius: 6px;
|
332
|
+
padding: 8px;
|
333
|
+
margin-top: 10px;
|
334
|
+
max-height: 200px;
|
335
|
+
overflow: auto;
|
336
|
+
}
|
337
|
+
|
338
|
+
/* ---------- Corner Positioning ---------- */
|
339
|
+
#command-deck-toggle.cd-pos-tl {
|
340
|
+
left: 16px;
|
341
|
+
right: auto;
|
342
|
+
top: 16px;
|
343
|
+
bottom: auto;
|
344
|
+
}
|
345
|
+
|
346
|
+
#command-deck-toggle.cd-pos-tr {
|
347
|
+
right: 16px;
|
348
|
+
left: auto;
|
349
|
+
top: 16px;
|
350
|
+
bottom: auto;
|
351
|
+
}
|
352
|
+
|
353
|
+
#command-deck-toggle.cd-pos-bl {
|
354
|
+
left: 16px;
|
355
|
+
right: auto;
|
356
|
+
bottom: 16px;
|
357
|
+
top: auto;
|
358
|
+
}
|
359
|
+
|
360
|
+
#command-deck-toggle.cd-pos-br {
|
361
|
+
right: 16px;
|
362
|
+
left: auto;
|
363
|
+
bottom: 16px;
|
364
|
+
top: auto;
|
365
|
+
}
|
366
|
+
|
367
|
+
#command-deck-panel.cd-pos-tl {
|
368
|
+
left: 16px;
|
369
|
+
right: auto;
|
370
|
+
top: 72px;
|
371
|
+
bottom: auto;
|
372
|
+
}
|
373
|
+
|
374
|
+
#command-deck-panel.cd-pos-tr {
|
375
|
+
right: 16px;
|
376
|
+
left: auto;
|
377
|
+
top: 72px;
|
378
|
+
bottom: auto;
|
379
|
+
}
|
380
|
+
|
381
|
+
#command-deck-panel.cd-pos-bl {
|
382
|
+
left: 16px;
|
383
|
+
right: auto;
|
384
|
+
bottom: 72px;
|
385
|
+
top: auto;
|
386
|
+
}
|
387
|
+
|
388
|
+
#command-deck-panel.cd-pos-br {
|
389
|
+
right: 16px;
|
390
|
+
left: auto;
|
391
|
+
bottom: 72px;
|
392
|
+
top: auto;
|
393
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
export class PanelsApi {
|
2
|
+
constructor(mount) { this.mount = mount; }
|
3
|
+
fetchPanels() {
|
4
|
+
return fetch(this.mount + '/panels', { credentials: 'same-origin' }).then(r => r.json());
|
5
|
+
}
|
6
|
+
}
|
7
|
+
|
8
|
+
export class ActionsApi {
|
9
|
+
constructor(mount) { this.mount = mount; }
|
10
|
+
submit(key, params) {
|
11
|
+
const tokenEl = document.querySelector('meta[name="csrf-token"]');
|
12
|
+
const token = tokenEl ? tokenEl.getAttribute('content') : '';
|
13
|
+
return fetch(this.mount + '/actions', {
|
14
|
+
method: 'POST',
|
15
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token },
|
16
|
+
body: JSON.stringify({ key, params }),
|
17
|
+
credentials: 'same-origin'
|
18
|
+
}).then(r => r.json());
|
19
|
+
}
|
20
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
// DOM helpers and ready hook
|
2
|
+
export function onReady(fn) {
|
3
|
+
const run = () => { try { fn(); } catch(_) {} };
|
4
|
+
if (document.readyState === 'loading') {
|
5
|
+
document.addEventListener('DOMContentLoaded', run);
|
6
|
+
} else {
|
7
|
+
run();
|
8
|
+
}
|
9
|
+
document.addEventListener('turbo:load', run);
|
10
|
+
document.addEventListener('turbo:render', run);
|
11
|
+
window.addEventListener('pageshow', run);
|
12
|
+
}
|
13
|
+
|
14
|
+
export function el(tag, attrs, children) {
|
15
|
+
const e = document.createElement(tag);
|
16
|
+
if (attrs) for (const k in attrs) {
|
17
|
+
if (k === 'style' && typeof attrs[k] === 'object') {
|
18
|
+
Object.assign(e.style, attrs[k]);
|
19
|
+
} else if (k === 'class') {
|
20
|
+
e.className = attrs[k];
|
21
|
+
} else if (k.startsWith('on') && typeof attrs[k] === 'function') {
|
22
|
+
e.addEventListener(k.slice(2), attrs[k]);
|
23
|
+
} else {
|
24
|
+
e.setAttribute(k, attrs[k]);
|
25
|
+
}
|
26
|
+
}
|
27
|
+
(children || []).forEach(c => e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c));
|
28
|
+
return e;
|
29
|
+
}
|
30
|
+
|
31
|
+
export function jsonPretty(obj) {
|
32
|
+
try { return JSON.stringify(obj, null, 2); } catch(_) { return String(obj); }
|
33
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
const KEY_PREFIX = 'command-deck-';
|
2
|
+
|
3
|
+
export const store = {
|
4
|
+
get(key, fallback = null) {
|
5
|
+
try {
|
6
|
+
const v = localStorage.getItem(KEY_PREFIX + key);
|
7
|
+
return v == null ? fallback : v;
|
8
|
+
} catch(_) { return fallback; }
|
9
|
+
},
|
10
|
+
set(key, value) {
|
11
|
+
try { localStorage.setItem(KEY_PREFIX + key, value); } catch(_) {}
|
12
|
+
}
|
13
|
+
};
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import { onReady } from './core/dom.js';
|
2
|
+
import { PanelsApi, ActionsApi } from './core/api.js';
|
3
|
+
import { Overlay } from './ui/overlay.js';
|
4
|
+
import { SettingsDropdown } from './ui/settings_dropdown.js';
|
5
|
+
import { PanelSelector } from './ui/panel_selector.js';
|
6
|
+
|
7
|
+
function getMount() {
|
8
|
+
const s = document.querySelector('script[data-mount]') || document.currentScript;
|
9
|
+
const m = s && s.getAttribute('data-mount');
|
10
|
+
return m || '/command_deck';
|
11
|
+
}
|
12
|
+
|
13
|
+
onReady(() => {
|
14
|
+
function ensureOverlay() {
|
15
|
+
const existingToggle = document.getElementById('command-deck-toggle');
|
16
|
+
const existingPanel = document.getElementById('command-deck-panel');
|
17
|
+
if (existingToggle && existingPanel) return;
|
18
|
+
|
19
|
+
const mount = getMount();
|
20
|
+
const panelsApi = new PanelsApi(mount);
|
21
|
+
const actionsApi = new ActionsApi(mount);
|
22
|
+
|
23
|
+
const overlay = new Overlay({ mount, panelsApi, actionsApi });
|
24
|
+
overlay.init();
|
25
|
+
|
26
|
+
const dropdown = new SettingsDropdown(
|
27
|
+
(pos) => overlay.position.set(pos),
|
28
|
+
(theme) => overlay.theme && overlay.theme.set(theme)
|
29
|
+
);
|
30
|
+
overlay.attachSettings(dropdown);
|
31
|
+
|
32
|
+
const selector = new PanelSelector((key) => overlay.selectPanel(key));
|
33
|
+
overlay.attachPanelSelector(selector);
|
34
|
+
|
35
|
+
overlay.loadPanels();
|
36
|
+
}
|
37
|
+
|
38
|
+
ensureOverlay();
|
39
|
+
});
|