command_deck 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05e42991e760e0f2cd8c5f77d013371c65093baa79ea95b5ae567cb3498384b3
4
- data.tar.gz: 5aee49684b906f405277f8c24b1b42c47baf76a69318758fcb70bbf628eccb9b
3
+ metadata.gz: 2f79a2f12ff19b218e7b02e3269ebaff3a283bc3772cdedeaffe829a7e3c9adc
4
+ data.tar.gz: 0b76ee1ba7f39d8b3a8a781ec33aefe32efdd9e5b66edcfced9e9fc77ea8dcfb
5
5
  SHA512:
6
- metadata.gz: 91677f4ec1316c0f39ba727346771555c59c01b56289152693a95464ed482e20a3ea34ac81c8522267190a60de8282786e673f2c6d9a247ef7210afa50c31f14
7
- data.tar.gz: a8345037758fd71e689570738f6208b35c40d3bce20a02ea6cf3c77821889b4c54aed9384fbef20cdb045dac77bdc30b5b343d188505e7f49eb1fd481edd5aa2
6
+ metadata.gz: 64a99c105eb4dbce276c748aca3781dbe8f74e5bec7b7ab6073e141a7e7fceac5680bfc4e8f67f1aaac25e2642914ef4d6d7df7e0d0e597614836326f652ec25
7
+ data.tar.gz: d9ce128531b79674de1d8125995969bf8d4e12df619e7b03aa52a569760f204d7a38fcc28e5cdce5604d9ae8b43fe82e2867a3e972724ddebfe9f5d71b2750b4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [0.1.2] - 2025-09-11
4
+
5
+ ### Added
6
+
7
+ - Support for Turbolinks.
8
+ - Copy to clipboard button for results.
9
+ - Toggle results visibility button.
10
+ - Refactor overlay.js into multiple files.
11
+
3
12
  ## [0.1.1] - 2025-09-10
4
13
 
5
14
  ### Fixed
@@ -324,6 +324,39 @@
324
324
  cursor: not-allowed;
325
325
  }
326
326
 
327
+ /* ---------- Results Toolbar ---------- */
328
+ #cd-result-wrap {
329
+ position: relative;
330
+ margin-top: 10px;
331
+ padding-top: 28px;
332
+ min-height: 28px;
333
+ }
334
+
335
+ .cd-result-toolbar {
336
+ position: absolute;
337
+ top: 6px;
338
+ right: 6px;
339
+ display: flex;
340
+ gap: 6px;
341
+ z-index: 1;
342
+ }
343
+
344
+ .cd-icon-btn {
345
+ border: 1px solid #ddd;
346
+ background: #f9f9f9;
347
+ border-radius: 6px;
348
+ padding: 2px 6px;
349
+ cursor: pointer;
350
+ font-size: 12px;
351
+ line-height: 1;
352
+ }
353
+
354
+ #command-deck-panel.cd-theme-dark .cd-icon-btn {
355
+ background: #111;
356
+ color: #eee;
357
+ border-color: #333;
358
+ }
359
+
327
360
  #command-deck-output {
328
361
  white-space: pre-wrap;
329
362
  background: #0b102114;
@@ -8,6 +8,8 @@ export function onReady(fn) {
8
8
  }
9
9
  document.addEventListener('turbo:load', run);
10
10
  document.addEventListener('turbo:render', run);
11
+ document.addEventListener('turbolinks:load', run);
12
+ document.addEventListener('turbolinks:render', run);
11
13
  window.addEventListener('pageshow', run);
12
14
  }
13
15
 
@@ -0,0 +1,187 @@
1
+ import { el } from '../../core/dom.js';
2
+ import { store } from '../../core/store.js';
3
+
4
+ export class ActionForm {
5
+ constructor({ action, getCurrentPanelKey, actionsApi, onRunning, onResult, onAfterRun }) {
6
+ this.action = action;
7
+ this.getCurrentPanelKey = getCurrentPanelKey;
8
+ this.actionsApi = actionsApi;
9
+ this.onRunning = onRunning || (() => {});
10
+ this.onResult = onResult || (() => {});
11
+ this.onAfterRun = onAfterRun || (() => {});
12
+ }
13
+
14
+ mount(parentNode) {
15
+ const box = el('div', { class: 'cd-action' });
16
+ const title = el('div', { class: 'cd-action-title' }, [this.action.title]);
17
+ const form = el('div', { class: 'cd-form' });
18
+
19
+ box.appendChild(title);
20
+ box.appendChild(form);
21
+
22
+ const inputs = {};
23
+ const paramsMeta = {};
24
+
25
+ (this.action.params || []).forEach(p => {
26
+ const labelChildren = [p.label || p.name];
27
+ if (p.required === true) {
28
+ labelChildren.push(el('span', { class: 'cd-required', title: 'Required' }, ['*']));
29
+ }
30
+ const label = el('label', null, labelChildren);
31
+ let input;
32
+ if (p.type === 'boolean') {
33
+ input = el('input', { type: 'checkbox' });
34
+ } else if (p.type === 'integer') {
35
+ input = el('input', { type: 'number', step: '1' });
36
+ } else if (p.type === 'selector' || (p.choices && p.choices.length)) {
37
+ input = el('select');
38
+ if (p.include_blank) {
39
+ input.appendChild(el('option', { value: '' }, ['']));
40
+ }
41
+ (p.choices || []).forEach(ch => {
42
+ const attrs = { 'data-val': JSON.stringify(ch.value) };
43
+ if (ch.meta != null) attrs['data-meta'] = JSON.stringify(ch.meta);
44
+ const opt = el('option', attrs, [ch.label != null ? String(ch.label) : String(ch.value)]);
45
+ input.appendChild(opt);
46
+ });
47
+
48
+ const selKey = `sel:${this.getCurrentPanelKey() || ''}:${this.action.key}:${p.name}`;
49
+ const savedRaw = store.get(selKey);
50
+ if (savedRaw != null) {
51
+ let matched = false;
52
+ for (let i = 0; i < input.options.length; i++) {
53
+ const o = input.options[i];
54
+ const raw = o.getAttribute('data-val');
55
+ if (raw === savedRaw) {
56
+ input.selectedIndex = i;
57
+ matched = true;
58
+ break;
59
+ }
60
+ }
61
+ if (!matched && p.include_blank && savedRaw === '') {
62
+ for (let i = 0; i < input.options.length; i++) {
63
+ if (input.options[i].value === '') { input.selectedIndex = i; break; }
64
+ }
65
+ }
66
+ }
67
+ } else {
68
+ input = el('input', { type: 'text' });
69
+ }
70
+ if (p.required === true && input) {
71
+ input.setAttribute('aria-required', 'true');
72
+ input.setAttribute('required', '');
73
+ }
74
+ form.appendChild(label);
75
+ form.appendChild(input);
76
+
77
+ let hint;
78
+ if (input.tagName === 'SELECT') {
79
+ hint = el('div', { class: 'cd-param-hint' });
80
+ const updateHint = () => {
81
+ const sel = input.options[input.selectedIndex];
82
+ if (!sel) { hint.textContent = ''; return; }
83
+ const metaRaw = sel.getAttribute('data-meta');
84
+ if (!metaRaw) { hint.textContent = ''; return; }
85
+ try {
86
+ const meta = JSON.parse(metaRaw);
87
+ if (typeof meta.enabled === 'boolean') {
88
+ const status = meta.enabled ? 'ON' : 'OFF';
89
+ hint.textContent = 'Current: ';
90
+ const badge = el('span', { class: `cd-badge ${meta.enabled ? 'cd-on' : 'cd-off'}` }, [status]);
91
+ hint.appendChild(badge);
92
+ } else {
93
+ hint.textContent = '';
94
+ }
95
+ } catch(_) { hint.textContent = ''; }
96
+ };
97
+ input.addEventListener('change', () => {
98
+ const sel = input.options[input.selectedIndex];
99
+ const raw = sel ? sel.getAttribute('data-val') : '';
100
+ const selKey = `sel:${this.getCurrentPanelKey() || ''}:${this.action.key}:${p.name}`;
101
+ store.set(selKey, raw || '');
102
+ updateHint();
103
+ validate();
104
+ });
105
+ setTimeout(updateHint, 0);
106
+ form.appendChild(hint);
107
+ }
108
+
109
+ inputs[p.name] = input;
110
+ paramsMeta[p.name] = p;
111
+ });
112
+
113
+ const requiredParams = (this.action.params || []).filter(p => p.required === true);
114
+ const isFilled = (p) => {
115
+ const inp = inputs[p.name];
116
+ if (!inp) return false;
117
+ if (p.type === 'integer') return inp.value !== '';
118
+ if (p.type === 'selector' || (p.choices && p.choices.length)) {
119
+ if (p.include_blank) return inp.value !== '' && inp.selectedIndex > -1;
120
+ return inp.selectedIndex > -1;
121
+ }
122
+ if (p.type === 'boolean') return true;
123
+ return String(inp.value || '').trim() !== '';
124
+ };
125
+ const validate = () => {
126
+ const ok = requiredParams.every(isFilled);
127
+ const running = run.classList.contains('loading');
128
+ run.disabled = !ok || running;
129
+ };
130
+
131
+ const run = el('button', { class: 'cd-run', title: 'Run' }, ['\u25B6']);
132
+ run.addEventListener('click', () => {
133
+ if (run.disabled) return;
134
+ run.classList.add('loading');
135
+ run.setAttribute('aria-busy', 'true');
136
+ run.disabled = true;
137
+
138
+ const payload = {};
139
+ Object.keys(inputs).forEach(k => {
140
+ const inp = inputs[k];
141
+ const meta = paramsMeta[k] || {};
142
+ if (inp.tagName === 'SELECT') {
143
+ const sel = inp.options[inp.selectedIndex];
144
+ const raw = sel && sel.getAttribute('data-val');
145
+ const label = sel ? sel.textContent : '';
146
+ let value;
147
+ try { value = raw != null ? JSON.parse(raw) : inp.value; } catch(_) { value = inp.value; }
148
+
149
+ if (meta.return === 'label') {
150
+ payload[k] = label;
151
+ } else if (meta.return === 'both' || meta.return === 'object') {
152
+ payload[k] = { label: label, value: value };
153
+ } else {
154
+ payload[k] = value;
155
+ }
156
+
157
+ const selKey = `sel:${this.getCurrentPanelKey() || ''}:${this.action.key}:${k}`;
158
+ store.set(selKey, raw || '');
159
+ } else {
160
+ payload[k] = (inp.type === 'checkbox') ? !!inp.checked : inp.value;
161
+ }
162
+ });
163
+
164
+ this.onRunning();
165
+ this.actionsApi
166
+ .submit(this.action.key, payload)
167
+ .then((res) => { this.onResult(res); this.onAfterRun(); })
168
+ .catch((e) => this.onResult({ ok:false, error: String(e) }))
169
+ .finally(() => { run.classList.remove('loading'); run.removeAttribute('aria-busy'); validate(); });
170
+ });
171
+
172
+ const actionsRow = el('div', { class: 'cd-actions-row' });
173
+ actionsRow.appendChild(run);
174
+ form.appendChild(actionsRow);
175
+
176
+ // Hook up validation listeners
177
+ (this.action.params || []).forEach(p => {
178
+ const inp = inputs[p.name];
179
+ if (!inp) return;
180
+ const evt = (inp.tagName === 'SELECT' || inp.type === 'checkbox') ? 'change' : 'input';
181
+ inp.addEventListener(evt, validate);
182
+ });
183
+ validate();
184
+
185
+ parentNode.appendChild(box);
186
+ }
187
+ }
@@ -0,0 +1,32 @@
1
+ import { ActionForm } from './action_form.js';
2
+
3
+ export class ActionsView {
4
+ constructor({ getCurrentPanelKey, actionsApi, onRunning, onResult, onAfterRun }) {
5
+ this.getCurrentPanelKey = getCurrentPanelKey;
6
+ this.actionsApi = actionsApi;
7
+ this.onRunning = onRunning || (() => {});
8
+ this.onResult = onResult || (() => {});
9
+ this.onAfterRun = onAfterRun || (() => {});
10
+ this.container = null;
11
+ }
12
+
13
+ mount(container) {
14
+ this.container = container;
15
+ }
16
+
17
+ setActions(actions) {
18
+ if (!this.container) return;
19
+ this.container.textContent = '';
20
+ (actions || []).forEach(action => {
21
+ const form = new ActionForm({
22
+ action,
23
+ getCurrentPanelKey: this.getCurrentPanelKey,
24
+ actionsApi: this.actionsApi,
25
+ onRunning: this.onRunning,
26
+ onResult: this.onResult,
27
+ onAfterRun: this.onAfterRun
28
+ });
29
+ form.mount(this.container);
30
+ });
31
+ }
32
+ }
@@ -0,0 +1,23 @@
1
+ import { el } from '../../core/dom.js';
2
+
3
+ export class HeaderView {
4
+ constructor() {
5
+ this.el = el('div', { id: 'cd-header' });
6
+ this.titleEl = el('h4', null, ['Command Deck']);
7
+ this.rightEl = el('div', { id: 'cd-header-right' });
8
+ this.el.appendChild(this.titleEl);
9
+ this.el.appendChild(this.rightEl);
10
+ }
11
+
12
+ mount(parent) {
13
+ parent.appendChild(this.el);
14
+ }
15
+
16
+ setTitle(text) {
17
+ this.titleEl.textContent = text;
18
+ }
19
+
20
+ getRightContainer() {
21
+ return this.rightEl;
22
+ }
23
+ }
@@ -1,4 +1,4 @@
1
- import { store } from '../core/store.js';
1
+ import { store } from '../../core/store.js';
2
2
 
3
3
  const POSITIONS = ['tl','tr','bl','br'];
4
4
 
@@ -0,0 +1,67 @@
1
+ import { el, jsonPretty } from '../../core/dom.js';
2
+ import { store } from '../../core/store.js';
3
+
4
+ export class ResultView {
5
+ constructor() {
6
+ this.wrap = el('div', { id: 'cd-result-wrap' });
7
+ this.toolbar = el('div', { class: 'cd-result-toolbar' });
8
+ this.copyBtn = el('button', { class: 'cd-icon-btn', id: 'cd-copy-result', title: 'Copy result' }, ['📋']);
9
+ this.toggleBtn = el('button', { class: 'cd-icon-btn', id: 'cd-toggle-result', title: 'Hide results' }, ['▾']);
10
+ this.pre = el('pre', { id: 'command-deck-output' });
11
+
12
+ this.toolbar.appendChild(this.copyBtn);
13
+ this.toolbar.appendChild(this.toggleBtn);
14
+ this.wrap.appendChild(this.toolbar);
15
+ this.wrap.appendChild(this.pre);
16
+
17
+ this.toggleBtn.addEventListener('click', () => {
18
+ const visible = this.pre.style.display === 'none' ? false : true;
19
+ this.setVisible(!visible);
20
+ });
21
+
22
+ this.copyBtn.addEventListener('click', async () => {
23
+ const text = this.pre.textContent || '';
24
+ try {
25
+ if (navigator.clipboard && navigator.clipboard.writeText) {
26
+ await navigator.clipboard.writeText(text);
27
+ } else {
28
+ const ta = document.createElement('textarea');
29
+ ta.value = text;
30
+ ta.style.position = 'fixed';
31
+ ta.style.opacity = '0';
32
+ document.body.appendChild(ta);
33
+ ta.select();
34
+ document.execCommand('copy');
35
+ document.body.removeChild(ta);
36
+ }
37
+ const prev = this.copyBtn.textContent;
38
+ this.copyBtn.textContent = '✓';
39
+ setTimeout(() => { this.copyBtn.textContent = prev; }, 1000);
40
+ } catch(_) { /* ignore */ }
41
+ });
42
+
43
+ const initialVis = store.get('results-visible', '1') !== '0';
44
+ this.setVisible(initialVis);
45
+ }
46
+
47
+ mount(parentNode) {
48
+ parentNode.appendChild(this.wrap);
49
+ }
50
+
51
+ setVisible(show) {
52
+ this.pre.style.display = show ? 'block' : 'none';
53
+ this.toggleBtn.textContent = show ? '▾' : '▸';
54
+ this.toggleBtn.setAttribute('title', show ? 'Hide results' : 'Show results');
55
+ store.set('results-visible', show ? '1' : '0');
56
+ }
57
+
58
+ setRunning() {
59
+ this.setVisible(true);
60
+ this.pre.textContent = 'Running...';
61
+ }
62
+
63
+ setResult(obj) {
64
+ this.setVisible(true);
65
+ this.pre.textContent = jsonPretty(obj);
66
+ }
67
+ }
@@ -0,0 +1,41 @@
1
+ import { el } from '../../core/dom.js';
2
+
3
+ export class TabsView {
4
+ constructor(onSelect) {
5
+ this.onSelect = onSelect;
6
+ this.root = el('div', { id: 'cd-tabs-wrap' });
7
+ this.buttons = [];
8
+ this.currentIndex = 0;
9
+ this.tabs = [];
10
+ }
11
+
12
+ mount(container) {
13
+ container.appendChild(this.root);
14
+ }
15
+
16
+ setTabs(tabs, selectedIndex = 0) {
17
+ this.tabs = tabs || [];
18
+ this.root.textContent = '';
19
+ this.buttons = [];
20
+ this.currentIndex = selectedIndex || 0;
21
+
22
+ this.tabs.forEach((tab, i) => {
23
+ const btn = el('button', { class: 'cd-tab-btn' }, [tab.title]);
24
+ btn.addEventListener('click', () => {
25
+ this.currentIndex = i;
26
+ this.onSelect && this.onSelect(this.tabs[i], i);
27
+ });
28
+ this.root.appendChild(btn);
29
+ this.buttons.push(btn);
30
+ });
31
+
32
+ // Trigger initial selection
33
+ if (this.tabs[this.currentIndex]) {
34
+ this.onSelect && this.onSelect(this.tabs[this.currentIndex], this.currentIndex);
35
+ }
36
+ }
37
+
38
+ setActiveIndex(i) {
39
+ this.currentIndex = i;
40
+ }
41
+ }
@@ -1,4 +1,4 @@
1
- import { store } from '../core/store.js';
1
+ import { store } from '../../core/store.js';
2
2
 
3
3
  export class ThemeManager {
4
4
  constructor(toggle, panel) {
@@ -1,7 +1,11 @@
1
- import { el, jsonPretty } from '../core/dom.js';
1
+ import { el } from '../core/dom.js';
2
2
  import { store } from '../core/store.js';
3
- import { PositionManager } from './position_manager.js';
4
- import { ThemeManager } from './theme_manager.js';
3
+ import { PositionManager } from './overlay/position_manager.js';
4
+ import { ThemeManager } from './overlay/theme_manager.js';
5
+ import { ResultView } from './overlay/result_view.js';
6
+ import { ActionsView } from './overlay/actions_view.js';
7
+ import { HeaderView } from './overlay/header_view.js';
8
+ import { TabsView } from './overlay/tabs_view.js';
5
9
 
6
10
  export class Overlay {
7
11
  constructor({ mount, panelsApi, actionsApi }) {
@@ -12,7 +16,6 @@ export class Overlay {
12
16
  }
13
17
 
14
18
  refreshCurrentTab() {
15
- // Re-fetch panels and re-render current panel/tab without changing selection
16
19
  this.panelsApi.fetchPanels().then((data) => {
17
20
  const panels = (data && data.panels) || [];
18
21
  this.panels = panels;
@@ -26,27 +29,31 @@ export class Overlay {
26
29
 
27
30
  init() {
28
31
  if (this._loaded) return; this._loaded = true;
29
- // Root pieces
30
32
  this.toggle = el('div', { id: 'command-deck-toggle', title: 'Command Deck' }, ['CD']);
31
33
  this.panel = el('div', { id: 'command-deck-panel', style: { display: 'none' } });
32
- // Keep these nodes across Turbo Drive navigations
33
34
  this.toggle.setAttribute('data-turbo-permanent', '');
34
35
  this.panel.setAttribute('data-turbo-permanent', '');
36
+ this.toggle.setAttribute('data-turbolinks-permanent', '');
37
+ this.panel.setAttribute('data-turbolinks-permanent', '');
35
38
 
36
- const headerWrap = el('div', { id: 'cd-header' });
37
- const title = el('h4', null, ['Command Deck']);
38
- headerWrap.appendChild(title);
39
- const headerRight = el('div', { id: 'cd-header-right' });
40
- headerWrap.appendChild(headerRight);
41
-
42
- this.tabsWrap = el('div', { id: 'cd-tabs-wrap' });
39
+ this.headerView = new HeaderView();
40
+ this.tabsView = new TabsView((tab, i) => { this.currentTabIndex = i; this.renderActions(tab); });
43
41
  this.actionsWrap = el('div', { id: 'cd-actions-wrap' });
44
- this.resultPre = el('pre', { id: 'command-deck-output' });
42
+ this.resultView = new ResultView();
43
+ this.actionsView = new ActionsView({
44
+ getCurrentPanelKey: () => this.currentPanelKey,
45
+ actionsApi: this.actionsApi,
46
+ onRunning: () => this.resultView.setRunning(),
47
+ onResult: (res) => this.resultView.setResult(res),
48
+ onAfterRun: () => this.refreshCurrentTab()
49
+ });
45
50
 
46
- this.panel.appendChild(headerWrap);
47
- this.panel.appendChild(this.tabsWrap);
51
+ this.headerView.mount(this.panel);
52
+ this.tabsView.mount(this.panel);
48
53
  this.panel.appendChild(this.actionsWrap);
49
- this.panel.appendChild(this.resultPre);
54
+ this.resultView.mount(this.panel);
55
+
56
+ this.actionsView.mount(this.actionsWrap);
50
57
 
51
58
  document.body.appendChild(this.toggle);
52
59
  document.body.appendChild(this.panel);
@@ -63,7 +70,6 @@ export class Overlay {
63
70
  }
64
71
 
65
72
  attachSettings(dropdown) {
66
- // Append settings into right side of header
67
73
  const headerRight = this.panel.querySelector('#cd-header-right');
68
74
  dropdown.mount(headerRight);
69
75
  if (this.theme) dropdown.setTheme && dropdown.setTheme(this.theme.load());
@@ -80,14 +86,15 @@ export class Overlay {
80
86
  }
81
87
 
82
88
  setPanels(data) {
83
- this.tabsWrap.textContent = '';
89
+ if (this.tabsView && this.tabsView.root) this.tabsView.root.textContent = '';
84
90
  this.actionsWrap.textContent = '';
85
91
  this.panels = (data && data.panels) || [];
86
92
  if (!this.panels.length) {
87
- this.tabsWrap.appendChild(el('div', null, ['No panels defined. Add files under app/command_deck/**/*.rb']))
93
+ if (this.tabsView && this.tabsView.root) {
94
+ this.tabsView.root.appendChild(el('div', null, ['No panels defined. Add files under app/command_deck/**/*.rb']))
95
+ }
88
96
  return;
89
97
  }
90
- // Initialize selector
91
98
  if (this.panelSelector) {
92
99
  this.panelSelector.setPanels(this.panels);
93
100
  }
@@ -110,196 +117,13 @@ export class Overlay {
110
117
  }
111
118
 
112
119
  renderTabs(panel, selectedIndex = 0) {
113
- this.tabsWrap.textContent = '';
120
+ if (this.tabsView && this.tabsView.root) this.tabsView.root.textContent = '';
114
121
  this.actionsWrap.textContent = '';
115
122
  this.currentTabIndex = selectedIndex;
116
- (panel.tabs || []).forEach((tab, i) => {
117
- const btn = el('button', { class: 'cd-tab-btn' }, [tab.title]);
118
- btn.addEventListener('click', () => { this.currentTabIndex = i; this.renderActions(tab); });
119
- this.tabsWrap.appendChild(btn);
120
- if (i === selectedIndex) this.renderActions(tab);
121
- });
123
+ this.tabsView.setTabs((panel && panel.tabs) || [], selectedIndex);
122
124
  }
123
125
 
124
126
  renderActions(tab) {
125
- this.actionsWrap.textContent = '';
126
- (tab.actions || []).forEach(action => {
127
- const box = el('div', { class: 'cd-action' });
128
- box.appendChild(el('div', { class: 'cd-action-title' }, [action.title]));
129
- const form = el('div', { class: 'cd-form' });
130
-
131
- const inputs = {};
132
- const paramsMeta = {};
133
- (action.params || []).forEach(p => {
134
- const labelChildren = [p.label || p.name];
135
- if (p.required === true) {
136
- labelChildren.push(el('span', { class: 'cd-required', title: 'Required' }, ['*']));
137
- }
138
- const label = el('label', null, labelChildren);
139
- let input;
140
- if (p.type === 'boolean') {
141
- input = el('input', { type: 'checkbox' });
142
- } else if (p.type === 'integer') {
143
- input = el('input', { type: 'number', step: '1' });
144
- } else if (p.type === 'selector' || (p.choices && p.choices.length)) {
145
- input = el('select');
146
- if (p.include_blank) {
147
- input.appendChild(el('option', { value: '' }, ['']));
148
- }
149
- (p.choices || []).forEach(ch => {
150
- const attrs = { 'data-val': JSON.stringify(ch.value) };
151
- if (ch.meta != null) attrs['data-meta'] = JSON.stringify(ch.meta);
152
- const opt = el('option', attrs, [ch.label != null ? String(ch.label) : String(ch.value)]);
153
- input.appendChild(opt);
154
- });
155
-
156
- // Restore last selection (per panel/action/param)
157
- const selKey = `sel:${this.currentPanelKey || ''}:${action.key}:${p.name}`;
158
- const savedRaw = store.get(selKey);
159
- if (savedRaw != null) {
160
- let matched = false;
161
- for (let i = 0; i < input.options.length; i++) {
162
- const o = input.options[i];
163
- const raw = o.getAttribute('data-val');
164
- if (raw === savedRaw) {
165
- input.selectedIndex = i;
166
- matched = true;
167
- break;
168
- }
169
- }
170
- if (!matched && p.include_blank && savedRaw === '') {
171
- for (let i = 0; i < input.options.length; i++) {
172
- if (input.options[i].value === '') { input.selectedIndex = i; break; }
173
- }
174
- }
175
- }
176
- } else {
177
- input = el('input', { type: 'text' });
178
- }
179
- if (p.required === true && input) {
180
- input.setAttribute('aria-required', 'true');
181
- input.setAttribute('required', '');
182
- }
183
- form.appendChild(label);
184
- form.appendChild(input);
185
-
186
- // Optional hint below inputs (used for selector current value display)
187
- let hint;
188
- if (input.tagName === 'SELECT') {
189
- hint = el('div', { class: 'cd-param-hint' });
190
- const updateHint = () => {
191
- const sel = input.options[input.selectedIndex];
192
- if (!sel) { hint.textContent = ''; return; }
193
- const metaRaw = sel.getAttribute('data-meta');
194
- if (!metaRaw) { hint.textContent = ''; return; }
195
- try {
196
- const meta = JSON.parse(metaRaw);
197
- if (typeof meta.enabled === 'boolean') {
198
- const status = meta.enabled ? 'ON' : 'OFF';
199
- hint.textContent = 'Current: ';
200
- const badge = el('span', { class: `cd-badge ${meta.enabled ? 'cd-on' : 'cd-off'}` }, [status]);
201
- hint.appendChild(badge);
202
- } else {
203
- hint.textContent = '';
204
- }
205
- } catch(_) { hint.textContent = ''; }
206
- };
207
- input.addEventListener('change', () => {
208
- // Persist selection
209
- const sel = input.options[input.selectedIndex];
210
- const raw = sel ? sel.getAttribute('data-val') : '';
211
- const selKey = `sel:${this.currentPanelKey || ''}:${action.key}:${p.name}`;
212
- store.set(selKey, raw || '');
213
- updateHint();
214
- validate();
215
- });
216
- // Initialize after options are appended
217
- setTimeout(updateHint, 0);
218
- form.appendChild(hint);
219
- }
220
-
221
- inputs[p.name] = input;
222
- paramsMeta[p.name] = p;
223
- });
224
-
225
- // Validation helpers
226
- const requiredParams = (action.params || []).filter(p => p.required === true);
227
- const isFilled = (p) => {
228
- const inp = inputs[p.name];
229
- if (!inp) return false;
230
- if (p.type === 'integer') return inp.value !== '';
231
- if (p.type === 'selector' || (p.choices && p.choices.length)) {
232
- if (p.include_blank) return inp.value !== '' && inp.selectedIndex > -1;
233
- return inp.selectedIndex > -1; // any selection counts
234
- }
235
- if (p.type === 'boolean') return true; // checkbox always provides a value
236
- // string or others
237
- return String(inp.value || '').trim() !== '';
238
- };
239
- const validate = () => {
240
- const ok = requiredParams.every(isFilled);
241
- const running = run.classList.contains('loading');
242
- run.disabled = !ok || running;
243
- };
244
-
245
- const run = el('button', { class: 'cd-run', title: 'Run' }, ['\u25B6']); // ▶
246
- run.addEventListener('click', () => {
247
- if (run.disabled) return;
248
- run.classList.add('loading');
249
- run.setAttribute('aria-busy', 'true');
250
- run.disabled = true;
251
- const payload = {};
252
- Object.keys(inputs).forEach(k => {
253
- const inp = inputs[k];
254
- const meta = paramsMeta[k] || {};
255
- if (inp.tagName === 'SELECT') {
256
- const sel = inp.options[inp.selectedIndex];
257
- const raw = sel && sel.getAttribute('data-val');
258
- const label = sel ? sel.textContent : '';
259
- let value;
260
- try { value = raw != null ? JSON.parse(raw) : inp.value; } catch(_) { value = inp.value; }
261
-
262
- if (meta.return === 'label') {
263
- payload[k] = label;
264
- } else if (meta.return === 'both' || meta.return === 'object') {
265
- payload[k] = { label: label, value: value };
266
- } else {
267
- payload[k] = value; // default: value
268
- }
269
-
270
- // Persist selection on run as well
271
- const selKey = `sel:${this.currentPanelKey || ''}:${action.key}:${k}`;
272
- store.set(selKey, raw || '');
273
- } else {
274
- payload[k] = (inp.type === 'checkbox') ? !!inp.checked : inp.value;
275
- }
276
- });
277
- this.setRunning();
278
- this.actionsApi
279
- .submit(action.key, payload)
280
- .then((res) => { this.setResult(res); this.refreshCurrentTab(); })
281
- .catch((e) => this.setResult({ ok:false, error: String(e) }))
282
- .finally(() => { run.classList.remove('loading'); run.removeAttribute('aria-busy'); validate(); });
283
- });
284
- const actionsRow = el('div', { class: 'cd-actions-row' });
285
- actionsRow.appendChild(run);
286
- form.appendChild(actionsRow);
287
-
288
- box.appendChild(form);
289
- this.actionsWrap.appendChild(box);
290
-
291
- // Hook up validation listeners
292
- (action.params || []).forEach(p => {
293
- const inp = inputs[p.name];
294
- if (!inp) return;
295
- const evt = (inp.tagName === 'SELECT' || inp.type === 'checkbox') ? 'change' : 'input';
296
- inp.addEventListener(evt, validate);
297
- });
298
- // Initial state
299
- validate();
300
- });
127
+ this.actionsView.setActions((tab && tab.actions) || []);
301
128
  }
302
-
303
- setRunning(){ this.resultPre.textContent = 'Running...'; }
304
- setResult(obj){ this.resultPre.textContent = jsonPretty(obj); }
305
129
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CommandDeck
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: command_deck
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - crowrojas
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-10 00:00:00.000000000 Z
11
+ date: 2025-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -65,10 +65,15 @@ files:
65
65
  - lib/command_deck/assets/js/core/store.js
66
66
  - lib/command_deck/assets/js/main.js
67
67
  - lib/command_deck/assets/js/ui/overlay.js
68
+ - lib/command_deck/assets/js/ui/overlay/action_form.js
69
+ - lib/command_deck/assets/js/ui/overlay/actions_view.js
70
+ - lib/command_deck/assets/js/ui/overlay/header_view.js
71
+ - lib/command_deck/assets/js/ui/overlay/position_manager.js
72
+ - lib/command_deck/assets/js/ui/overlay/result_view.js
73
+ - lib/command_deck/assets/js/ui/overlay/tabs_view.js
74
+ - lib/command_deck/assets/js/ui/overlay/theme_manager.js
68
75
  - lib/command_deck/assets/js/ui/panel_selector.js
69
- - lib/command_deck/assets/js/ui/position_manager.js
70
76
  - lib/command_deck/assets/js/ui/settings_dropdown.js
71
- - lib/command_deck/assets/js/ui/theme_manager.js
72
77
  - lib/command_deck/engine.rb
73
78
  - lib/command_deck/executor.rb
74
79
  - lib/command_deck/injector.rb