json-object-editor 0.10.660 → 0.10.663
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/CHANGELOG.md +16 -0
- package/_www/mcp-nav.js +1 -0
- package/_www/plugins-test.html +333 -0
- package/css/joe-styles.css +1 -1
- package/css/joe.css +2 -2
- package/css/joe.min.css +1 -1
- package/docs/JOE_AI_Overview.md +74 -18
- package/docs/React_Form_Integration_Example.md +299 -0
- package/docs/React_Form_Integration_Strategy.md +5 -6
- package/dummy +9 -1
- package/js/joe-ai.js +26 -0
- package/js/joe-react-form.js +608 -0
- package/js/joe.js +1 -1
- package/package.json +1 -1
- package/readme.md +12 -0
- package/server/fields/core.js +48 -1
- package/server/modules/MCP.js +4 -0
- package/server/modules/Server.js +31 -10
- package/server/modules/Sites.js +28 -2
- package/server/plugins/chatgpt.js +169 -21
- package/server/plugins/formBuilder.js +98 -0
- package/server/plugins/plugin-utils.js +28 -10
- package/server/schemas/ai_assistant.js +43 -44
- package/server/schemas/ai_prompt.js +4 -58
- package/server/schemas/include.js +9 -4
- package/server/schemas/page.js +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 0.10.663
|
|
2
|
+
|
|
3
|
+
- Plugin inventory debug page
|
|
4
|
+
- Added `_www/plugins-test.html`, a secured debug page that lists all active plugins, their async/top-level methods, and which apps use them, sourced from the `plugin-utils` plugin.
|
|
5
|
+
- This page is primarily intended for AI agents and developers to quickly verify that methods like `chatgpt.autofill` and `chatgpt.widgetStart` exist on a given instance and to inspect plugin wiring per app.
|
|
6
|
+
|
|
7
|
+
## 0.10.662
|
|
8
|
+
|
|
9
|
+
- React Form Integration with JSON Definitions
|
|
10
|
+
- **JSON includes**: Extended `include` schema to support `filetype: 'json'` for storing JSON form definitions; JSON includes are served with `Content-Type: application/json` at `/_include/{id}`.
|
|
11
|
+
- **Form-page linking**: Added `form` field (reference) to `page` schema, allowing pages to link to JOE forms for dynamic form rendering.
|
|
12
|
+
- **Form definition API**: Added `formBuilder.definition()` method (merged from separate `formDefinition` plugin) to serve JSON form definitions via `/API/plugin/formBuilder/definition?formid={id}&pageid={id}`. Automatically finds JSON includes from form metadata, form fields, or page includes.
|
|
13
|
+
- **React form renderer**: Created `js/joe-react-form.js` client-side library that renders multi-step React forms from JSON definitions. Supports conditional visibility, validation, and submits to existing `/API/plugin/formBuilder/submission` endpoint.
|
|
14
|
+
- **Page content rendering**: Enhanced `Sites.js` to preserve newlines in page content when processing template variables, fixing issues with JavaScript comments in `code` and `module` content types.
|
|
15
|
+
- **Documentation**: Added `docs/React_Form_Integration_Strategy.md` and `docs/React_Form_Integration_Example.md` with strategy options and implementation examples.
|
|
16
|
+
|
|
1
17
|
## 0.10.660
|
|
2
18
|
|
|
3
19
|
- MCP as an optional layer on all AI surfaces
|
package/_www/mcp-nav.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
'<a href="/matrix.html" target="matrix_win" rel="noopener">Matrix</a>',
|
|
12
12
|
'<a href="/mcp-prompt.html" target="mcp_prompt_win" rel="noopener">MCP Prompt</a>',
|
|
13
13
|
'<a href="/ai-widget-test.html" target="ai_widget_test_win" rel="noopener">AI Widget</a>',
|
|
14
|
+
'<a href="/plugins-test.html" target="plugins_test_win" rel="noopener">Plugins</a>',
|
|
14
15
|
'<span style="margin-left:auto"></span>',
|
|
15
16
|
'<a href="/privacy" target="privacy_win" rel="noopener">Privacy</a>',
|
|
16
17
|
'<a href="/terms" target="terms_win" rel="noopener">Terms</a>',
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>JOE Plugins — Inventory</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<style>
|
|
8
|
+
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px}
|
|
9
|
+
h1{margin:8px 0}
|
|
10
|
+
.small{font-size:12px;color:#666}
|
|
11
|
+
table{width:100%;border-collapse:collapse;margin-top:10px}
|
|
12
|
+
th,td{border:1px solid #e1e4e8;padding:8px;text-align:left;font-size:14px}
|
|
13
|
+
th.sortable{cursor:pointer}
|
|
14
|
+
th.sorted-asc, th.sorted-desc{background:#eef}
|
|
15
|
+
th .sort-ind{font-size:12px;opacity:.6;margin-left:6px}
|
|
16
|
+
.chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
17
|
+
.chip{display:inline-block;padding:2px 8px;border-radius:12px;background:#eef;border:1px solid #ccd;font-size:12px}
|
|
18
|
+
.chip.joe{background:#11bcd6;color:#fff;border-color:#0fa5bb}
|
|
19
|
+
.chip.app{background:#e0ecff;border-color:#b3c7ff}
|
|
20
|
+
.chip.async{background:#dcfce7;border-color:#16a34a}
|
|
21
|
+
.chip.protected{background:#fee2e2;border-color:#dc2626}
|
|
22
|
+
.badge{display:inline-block;padding:2px 6px;border-radius:10px;font-size:12px;background:#eef;border:1px solid #ccd}
|
|
23
|
+
.badge.warn{background:#f0c040;color:#000;border-color:#e5b93b}
|
|
24
|
+
.badge.ok{background:#0a7d00;color:#fff;border-color:#0a7d00}
|
|
25
|
+
.badge.info{background:#e5e7eb;color:#111827;border-color:#d1d5db}
|
|
26
|
+
.t-center{text-align:center}
|
|
27
|
+
th{background:#f6f8fa}
|
|
28
|
+
.status-ok{color:#0a7d00;font-size:22px}
|
|
29
|
+
.status-bad{color:#b00020;font-size:22px}
|
|
30
|
+
.row{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:8px 0}
|
|
31
|
+
a[target]{text-decoration:none}
|
|
32
|
+
code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px}
|
|
33
|
+
</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<div id="mcp-nav"></div>
|
|
37
|
+
<script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
|
|
38
|
+
|
|
39
|
+
<h1>Plugin Inventory</h1>
|
|
40
|
+
<div class="small">
|
|
41
|
+
Overview of active JOE plugins and their async methods, with app usage.
|
|
42
|
+
Useful for verifying that methods like <code>chatgpt.autofill</code> and
|
|
43
|
+
<code>chatgpt.widgetStart</code> are present on this instance.
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="row" style="margin-top:12px;">
|
|
47
|
+
<label for="base" style="margin:0">Base URL</label>
|
|
48
|
+
<input id="base" value="" placeholder="http://localhost:2025" style="min-width:260px" />
|
|
49
|
+
<button id="refresh">Refresh</button>
|
|
50
|
+
<span id="status" class="small"></span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="row" style="margin-top:4px;">
|
|
54
|
+
<label for="appFilter" style="margin:0">Filter by App</label>
|
|
55
|
+
<select id="appFilter">
|
|
56
|
+
<option value="">All</option>
|
|
57
|
+
</select>
|
|
58
|
+
<label for="nameFilter" style="margin:0">Filter by Plugin</label>
|
|
59
|
+
<input id="nameFilter" placeholder="plugin name contains..." style="min-width:200px" />
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<table>
|
|
63
|
+
<thead>
|
|
64
|
+
<tr>
|
|
65
|
+
<th class="sortable" data-key="name">Plugin <span class="sort-ind"></span></th>
|
|
66
|
+
<th>Path</th>
|
|
67
|
+
<th class="sortable" data-key="methodCount">Async Methods <span class="sort-ind"></span></th>
|
|
68
|
+
<th>Methods</th>
|
|
69
|
+
<th>Apps Using</th>
|
|
70
|
+
<th class="sortable" data-key="protectedCount">Protected <span class="sort-ind"></span></th>
|
|
71
|
+
<th>Notes</th>
|
|
72
|
+
</tr>
|
|
73
|
+
</thead>
|
|
74
|
+
<tbody id="rows"></tbody>
|
|
75
|
+
</table>
|
|
76
|
+
|
|
77
|
+
<script>
|
|
78
|
+
(function(){
|
|
79
|
+
var $ = function(id){ return document.getElementById(id); };
|
|
80
|
+
var base = $('base');
|
|
81
|
+
var refresh = $('refresh');
|
|
82
|
+
var status = $('status');
|
|
83
|
+
var rows = $('rows');
|
|
84
|
+
var appFilterSel = $('appFilter');
|
|
85
|
+
var nameFilter = $('nameFilter');
|
|
86
|
+
base.value = base.value || location.origin;
|
|
87
|
+
|
|
88
|
+
function setStatus(msg, ok){
|
|
89
|
+
status.textContent = msg||'';
|
|
90
|
+
status.className = 'small ' + (ok===true?'status-ok': ok===false?'status-bad':'');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchJSON(url, opts){
|
|
94
|
+
const res = await fetch(url, opts);
|
|
95
|
+
if(!res.ok){
|
|
96
|
+
let body;
|
|
97
|
+
try{ body = await res.text(); }catch(_e){ body=''; }
|
|
98
|
+
throw new Error('HTTP '+res.status+' '+body);
|
|
99
|
+
}
|
|
100
|
+
return res.json();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function callMCP(baseUrl, method, params){
|
|
104
|
+
const url = baseUrl + '/mcp';
|
|
105
|
+
const body = { jsonrpc:'2.0', id:String(Date.now()), method:method, params:params||{} };
|
|
106
|
+
const resp = await fetch(url, {
|
|
107
|
+
method:'POST',
|
|
108
|
+
headers:{'Content-Type':'application/json'},
|
|
109
|
+
body: JSON.stringify(body)
|
|
110
|
+
});
|
|
111
|
+
if(!resp.ok){
|
|
112
|
+
throw new Error('HTTP '+resp.status+' '+(await resp.text()));
|
|
113
|
+
}
|
|
114
|
+
const j = await resp.json();
|
|
115
|
+
return (j && (j.result || j)) || {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function load(){
|
|
119
|
+
try{
|
|
120
|
+
setStatus('Loading...', null);
|
|
121
|
+
rows.innerHTML = '';
|
|
122
|
+
var baseUrl = base.value.replace(/\/$/,'');
|
|
123
|
+
|
|
124
|
+
// 1) Load plugin list via plugin-utils
|
|
125
|
+
var plugData = await fetchJSON(baseUrl + '/API/plugin/plugin-utils');
|
|
126
|
+
var plugins = (plugData && plugData.plugins) || {};
|
|
127
|
+
|
|
128
|
+
// 2) Load apps via MCP listApps so we can see which apps reference which plugins
|
|
129
|
+
var appMap = {};
|
|
130
|
+
try{
|
|
131
|
+
var la = await callMCP(baseUrl, 'listApps', {});
|
|
132
|
+
appMap = (la && la.apps) || {};
|
|
133
|
+
}catch(_e){ appMap = {}; }
|
|
134
|
+
|
|
135
|
+
// Build app -> plugins map and plugin -> apps usage map
|
|
136
|
+
var pluginApps = {}; // { pluginName: [appName,...] }
|
|
137
|
+
Object.keys(appMap || {}).forEach(function(appName){
|
|
138
|
+
var app = appMap[appName] || {};
|
|
139
|
+
var appPlugins = Array.isArray(app.plugins) ? app.plugins : [];
|
|
140
|
+
appPlugins.forEach(function(p){
|
|
141
|
+
if(!p) return;
|
|
142
|
+
pluginApps[p] = pluginApps[p] || [];
|
|
143
|
+
if (pluginApps[p].indexOf(appName) === -1){
|
|
144
|
+
pluginApps[p].push(appName);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Populate app filter options
|
|
150
|
+
if (appFilterSel){
|
|
151
|
+
// Clear existing (keep "All")
|
|
152
|
+
while(appFilterSel.options.length > 1){
|
|
153
|
+
appFilterSel.remove(1);
|
|
154
|
+
}
|
|
155
|
+
Object.keys(appMap || {}).sort().forEach(function(a){
|
|
156
|
+
var opt=document.createElement('option');
|
|
157
|
+
opt.value=a;
|
|
158
|
+
opt.textContent=a;
|
|
159
|
+
appFilterSel.appendChild(opt);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Normalize plugin rows
|
|
164
|
+
var data = Object.keys(plugins || {}).sort().map(function(name){
|
|
165
|
+
var p = plugins[name] || {};
|
|
166
|
+
var asyncMethods = Array.isArray(p.async) ? p.async.slice().sort() : [];
|
|
167
|
+
var topLevelMethods = Array.isArray(p.methods) ? p.methods.slice().sort() : [];
|
|
168
|
+
var usedBy = (pluginApps[name] || []).slice().sort();
|
|
169
|
+
var protectedList = Array.isArray(p.protected) ? p.protected : [];
|
|
170
|
+
// Union of async/top-level/protected so we show everything.
|
|
171
|
+
var methodSet = {};
|
|
172
|
+
asyncMethods.forEach(function(m){ if(m){ methodSet[m] = true; }});
|
|
173
|
+
topLevelMethods.forEach(function(m){ if(m){ methodSet[m] = true; }});
|
|
174
|
+
protectedList.forEach(function(m){ if(m){ methodSet[m] = true; }});
|
|
175
|
+
var allMethods = Object.keys(methodSet).sort();
|
|
176
|
+
return {
|
|
177
|
+
name: name,
|
|
178
|
+
path: p._pathname || '',
|
|
179
|
+
override: !!p._override,
|
|
180
|
+
methodsAll: allMethods,
|
|
181
|
+
asyncMethods: asyncMethods,
|
|
182
|
+
methodCount: asyncMethods.length,
|
|
183
|
+
usedBy: usedBy,
|
|
184
|
+
protected: protectedList,
|
|
185
|
+
protectedCount: protectedList.length
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
function getFilteredData(){
|
|
190
|
+
var appSel = (appFilterSel && appFilterSel.value) || '';
|
|
191
|
+
var nameSel = (nameFilter && nameFilter.value || '').toLowerCase().trim();
|
|
192
|
+
return data.filter(function(d){
|
|
193
|
+
if (appSel && (!d.usedBy || d.usedBy.indexOf(appSel) === -1)){
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (nameSel && d.name.toLowerCase().indexOf(nameSel) === -1){
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
var sortState = { key:'name', dir:'asc' };
|
|
204
|
+
function sortData(key){
|
|
205
|
+
if (sortState.key === key){ sortState.dir = (sortState.dir === 'asc') ? 'desc' : 'asc'; }
|
|
206
|
+
else {
|
|
207
|
+
sortState.key = key;
|
|
208
|
+
sortState.dir = (key === 'methodCount' || key === 'protectedCount') ? 'desc' : 'asc';
|
|
209
|
+
}
|
|
210
|
+
var dir = sortState.dir === 'asc' ? 1 : -1;
|
|
211
|
+
var baseList = getFilteredData();
|
|
212
|
+
var sorted = baseList.slice().sort(function(a,b){
|
|
213
|
+
var av = a[key], bv = b[key];
|
|
214
|
+
if (typeof av === 'string' && typeof bv === 'string'){
|
|
215
|
+
av = av.toLowerCase(); bv = bv.toLowerCase();
|
|
216
|
+
}
|
|
217
|
+
if (av > bv) return 1*dir;
|
|
218
|
+
if (av < bv) return -1*dir;
|
|
219
|
+
return 0;
|
|
220
|
+
});
|
|
221
|
+
renderTable(sorted);
|
|
222
|
+
updateHeaderIndicators();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function updateHeaderIndicators(){
|
|
226
|
+
var ths = document.querySelectorAll('th.sortable');
|
|
227
|
+
Array.prototype.forEach.call(ths, function(th){
|
|
228
|
+
th.classList.remove('sorted-asc','sorted-desc');
|
|
229
|
+
var ind = th.querySelector('.sort-ind');
|
|
230
|
+
if (ind) ind.textContent = '';
|
|
231
|
+
if (th.getAttribute('data-key') === sortState.key){
|
|
232
|
+
th.classList.add(sortState.dir === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
|
233
|
+
if (ind) ind.textContent = sortState.dir === 'asc' ? '▲' : '▼';
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function renderTable(list){
|
|
239
|
+
rows.innerHTML = '';
|
|
240
|
+
(list||[]).forEach(function(item){
|
|
241
|
+
var tr = document.createElement('tr');
|
|
242
|
+
function td(html, cls){
|
|
243
|
+
var c=document.createElement('td');
|
|
244
|
+
if(cls){c.className=cls;}
|
|
245
|
+
c.innerHTML = html||'';
|
|
246
|
+
return c;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Plugin name
|
|
250
|
+
tr.appendChild(td('<strong>'+item.name+'</strong>'));
|
|
251
|
+
|
|
252
|
+
// Path
|
|
253
|
+
tr.appendChild(td(item.path ? ('<code>'+item.path+'</code>') : '<span class="small">n/a</span>'));
|
|
254
|
+
|
|
255
|
+
// Async method count
|
|
256
|
+
tr.appendChild(td(String(item.methodCount), 't-center'));
|
|
257
|
+
|
|
258
|
+
// Methods list (all top-level + async). Color-code:
|
|
259
|
+
// - async methods: green chip
|
|
260
|
+
// - protected methods: red chip
|
|
261
|
+
var methodsHTML;
|
|
262
|
+
if (item.methodsAll && item.methodsAll.length){
|
|
263
|
+
methodsHTML = '<div class="chips">'+item.methodsAll.map(function(m){
|
|
264
|
+
var classes = ['chip'];
|
|
265
|
+
if (item.asyncMethods && item.asyncMethods.indexOf(m) !== -1){
|
|
266
|
+
classes.push('async');
|
|
267
|
+
}
|
|
268
|
+
if (item.protected && item.protected.indexOf(m) !== -1){
|
|
269
|
+
classes.push('protected');
|
|
270
|
+
}
|
|
271
|
+
return '<span class="'+classes.join(' ')+'">'+m+'</span>';
|
|
272
|
+
}).join(' ') + '</div>';
|
|
273
|
+
} else {
|
|
274
|
+
methodsHTML = '<span class="small">None</span>';
|
|
275
|
+
}
|
|
276
|
+
tr.appendChild(td(methodsHTML));
|
|
277
|
+
|
|
278
|
+
// Apps using
|
|
279
|
+
var appsHTML = item.usedBy.length
|
|
280
|
+
? '<div class="chips">'+item.usedBy.map(function(app){
|
|
281
|
+
var cls = 'chip app';
|
|
282
|
+
var link = '/JOE/'+app;
|
|
283
|
+
return '<a class="'+cls+'" href="'+link+'" target="_app_'+app+'">'+app+'</a>';
|
|
284
|
+
}).join(' ')+'</div>'
|
|
285
|
+
: '<span class="small">None</span>';
|
|
286
|
+
tr.appendChild(td(appsHTML));
|
|
287
|
+
|
|
288
|
+
// Protected list
|
|
289
|
+
var protHTML = item.protectedCount
|
|
290
|
+
? '<div class="chips">'+item.protected.map(function(m){ return '<span class="chip">'+m+'</span>'; }).join(' ')+'</div>'
|
|
291
|
+
: '<span class="small">None</span>';
|
|
292
|
+
tr.appendChild(td(protHTML, 't-center'));
|
|
293
|
+
|
|
294
|
+
// Notes
|
|
295
|
+
var notes = [];
|
|
296
|
+
if (item.override){
|
|
297
|
+
notes.push('<span class="badge warn">override</span>');
|
|
298
|
+
}
|
|
299
|
+
if (item.name === 'chatgpt'){
|
|
300
|
+
notes.push('<span class="badge info">AI / Responses+MCP</span>');
|
|
301
|
+
}
|
|
302
|
+
tr.appendChild(td(notes.join(' ') || '<span class="small">—</span>'));
|
|
303
|
+
|
|
304
|
+
rows.appendChild(tr);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Attach header sort handlers once
|
|
309
|
+
Array.prototype.forEach.call(document.querySelectorAll('th.sortable'), function(th){
|
|
310
|
+
th.onclick = function(){ sortData(th.getAttribute('data-key')); };
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (appFilterSel){
|
|
314
|
+
appFilterSel.onchange = function(){ sortData(sortState.key || 'name'); };
|
|
315
|
+
}
|
|
316
|
+
if (nameFilter){
|
|
317
|
+
nameFilter.oninput = function(){ sortData(sortState.key || 'name'); };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
sortData('name');
|
|
321
|
+
setStatus('Loaded '+Object.keys(plugins||{}).length+' plugins', true);
|
|
322
|
+
}catch(e){
|
|
323
|
+
setStatus(e.message||String(e), false);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
refresh.onclick = load;
|
|
328
|
+
setTimeout(load, 50);
|
|
329
|
+
})();
|
|
330
|
+
</script>
|
|
331
|
+
</body>
|
|
332
|
+
</html>
|
|
333
|
+
|
package/css/joe-styles.css
CHANGED
package/css/joe.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* --------------------------------------------------------
|
|
2
2
|
*
|
|
3
|
-
* json-object-editor - v0.10.
|
|
3
|
+
* json-object-editor - v0.10.661
|
|
4
4
|
* Created by: Corey Hadden
|
|
5
5
|
*
|
|
6
6
|
* -------------------------------------------------------- */
|
|
@@ -785,7 +785,7 @@ joe-button{
|
|
|
785
785
|
.joe-button {
|
|
786
786
|
padding: 8px 16px;
|
|
787
787
|
margin: 5px;
|
|
788
|
-
background-color:#
|
|
788
|
+
background-color:#cecece;
|
|
789
789
|
box-shadow: 0 0px 2px 0px rgba(0,0,0,.1);
|
|
790
790
|
cursor: pointer;
|
|
791
791
|
transition: .2s;
|