Spaces:
Running on Zero
Running on Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -3,7 +3,6 @@ Agentic Coding : 3D Camera View Generator
|
|
| 3 |
- Qwen Image Edit + Lightning LoRA + Multi-Angle LoRA
|
| 4 |
- gr.HTML custom component (Gradio 6)
|
| 5 |
- ZeroGPU (HuggingFace Spaces)
|
| 6 |
-
|
| 7 |
"""
|
| 8 |
|
| 9 |
import gradio as gr
|
|
@@ -125,16 +124,15 @@ HTML_TEMPLATE = """
|
|
| 125 |
{{#if value.img}}
|
| 126 |
<img class="cv-img" src="{{value.img}}">
|
| 127 |
{{else}}
|
| 128 |
-
<div class="cv-empty">
|
| 129 |
<svg class="cv-empty-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.25">
|
| 130 |
-
<path stroke-linecap="round" stroke-linejoin="round" d="
|
| 131 |
-
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z" />
|
| 132 |
</svg>
|
| 133 |
-
<p class="cv-empty-title">
|
| 134 |
-
<p class="cv-empty-sub">
|
|
|
|
| 135 |
</div>
|
| 136 |
{{/if}}
|
| 137 |
-
|
| 138 |
<div class="cv-hud">
|
| 139 |
<div class="cv-readout">
|
| 140 |
<span class="cv-lbl">Az</span><span class="cv-val">${value.az}°</span>
|
|
@@ -162,7 +160,6 @@ HTML_TEMPLATE = """
|
|
| 162 |
|
| 163 |
CSS_TEMPLATE = """
|
| 164 |
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 165 |
-
|
| 166 |
/* ββ Image well ββ dark neutral so images pop, same treatment as any
|
| 167 |
professional image editor / camera app preview area. Not a stylistic
|
| 168 |
choice but a functional one: images render best against dark. */
|
|
@@ -174,12 +171,10 @@ CSS_TEMPLATE = """
|
|
| 174 |
overflow: hidden;
|
| 175 |
display: flex; align-items: center; justify-content: center;
|
| 176 |
}
|
| 177 |
-
|
| 178 |
.cv-img {
|
| 179 |
max-width: 100%; max-height: 100%;
|
| 180 |
object-fit: contain; display: block;
|
| 181 |
}
|
| 182 |
-
|
| 183 |
/* empty state */
|
| 184 |
.cv-empty {
|
| 185 |
text-align: center; user-select: none;
|
|
@@ -197,7 +192,26 @@ CSS_TEMPLATE = """
|
|
| 197 |
font-size: 13px; max-width: 230px; line-height: 1.65;
|
| 198 |
color: rgba(255,255,255,0.25);
|
| 199 |
}
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
/* HUD β fades in on hover via CSS, no JS needed */
|
| 202 |
.cv-hud {
|
| 203 |
position: absolute; bottom: 16px; right: 16px;
|
|
@@ -205,7 +219,6 @@ CSS_TEMPLATE = """
|
|
| 205 |
opacity: 0; transition: opacity 0.16s ease; pointer-events: auto;
|
| 206 |
}
|
| 207 |
.cv-wrap:hover .cv-hud { opacity: 1; }
|
| 208 |
-
|
| 209 |
/* coordinate readout β white card floating over image */
|
| 210 |
.cv-readout {
|
| 211 |
display: flex; align-items: center; gap: 8px;
|
|
@@ -217,7 +230,6 @@ CSS_TEMPLATE = """
|
|
| 217 |
.cv-lbl { color: #9ca3af; font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; }
|
| 218 |
.cv-val { color: #111827; font-weight: 600; font-variant-numeric: tabular-nums; }
|
| 219 |
.cv-sep { color: #d1d5db; margin: 0 2px; }
|
| 220 |
-
|
| 221 |
/* controls panel β white card, same treatment as readout */
|
| 222 |
.cv-controls {
|
| 223 |
display: flex; align-items: center; gap: 8px;
|
|
@@ -225,7 +237,6 @@ CSS_TEMPLATE = """
|
|
| 225 |
border-radius: 10px; padding: 8px 10px;
|
| 226 |
box-shadow: 0 2px 12px rgba(0,0,0,0.25);
|
| 227 |
}
|
| 228 |
-
|
| 229 |
/* d-pad */
|
| 230 |
.cv-dpad {
|
| 231 |
display: grid;
|
|
@@ -248,7 +259,6 @@ CSS_TEMPLATE = """
|
|
| 248 |
transform: scale(1.1);
|
| 249 |
}
|
| 250 |
.cv-btn:active { transform: scale(0.92); background: #ffedd5; }
|
| 251 |
-
|
| 252 |
.cv-up { grid-column:2; grid-row:1; }
|
| 253 |
.cv-left { grid-column:1; grid-row:2; }
|
| 254 |
.cv-dot {
|
|
@@ -258,7 +268,6 @@ CSS_TEMPLATE = """
|
|
| 258 |
}
|
| 259 |
.cv-right { grid-column:3; grid-row:2; }
|
| 260 |
.cv-down { grid-column:2; grid-row:3; }
|
| 261 |
-
|
| 262 |
/* zoom column */
|
| 263 |
.cv-zoom { display: flex; flex-direction: column; gap: 3px; }
|
| 264 |
.cv-zbtn {
|
|
@@ -279,7 +288,6 @@ CSS_TEMPLATE = """
|
|
| 279 |
|
| 280 |
JS_ON_LOAD = """
|
| 281 |
const DIST_STEPS = [0.6, 1.0, 1.8];
|
| 282 |
-
|
| 283 |
function snapDist(d) {
|
| 284 |
return DIST_STEPS.reduce((p, c) => Math.abs(c - d) < Math.abs(p - d) ? c : p);
|
| 285 |
}
|
|
@@ -287,17 +295,53 @@ function shiftDist(d, dir) {
|
|
| 287 |
const idx = DIST_STEPS.indexOf(snapDist(Number(d)));
|
| 288 |
return DIST_STEPS[Math.max(0, Math.min(DIST_STEPS.length - 1, idx + dir))];
|
| 289 |
}
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
// Delegated click listener β attached once, survives template re-renders.
|
| 292 |
element.addEventListener('click', function(e) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
const btn = e.target.closest('[data-action]');
|
| 294 |
if (!btn) return;
|
| 295 |
-
|
| 296 |
const v = Object.assign({}, props.value);
|
| 297 |
let az = Number(v.az) || 0;
|
| 298 |
let el = Number(v.el) || 0;
|
| 299 |
let dist = Number(v.dist) || 1.0;
|
| 300 |
-
|
| 301 |
switch (btn.dataset.action) {
|
| 302 |
case 'az-minus': az = (az - 45 + 360) % 360; break;
|
| 303 |
case 'az-plus': az = (az + 45) % 360; break;
|
|
@@ -306,7 +350,6 @@ element.addEventListener('click', function(e) {
|
|
| 306 |
case 'dist-minus': dist = shiftDist(dist, -1); break;
|
| 307 |
case 'dist-plus': dist = shiftDist(dist, +1); break;
|
| 308 |
}
|
| 309 |
-
|
| 310 |
props.value = { ...v, az, el, dist };
|
| 311 |
trigger('submit');
|
| 312 |
});
|
|
@@ -321,7 +364,6 @@ GLOBAL_CSS = """
|
|
| 321 |
.gradio-container .row {
|
| 322 |
flex-wrap: nowrap !important;
|
| 323 |
}
|
| 324 |
-
|
| 325 |
/* ββ Header ββ */
|
| 326 |
.app-heading { padding: 28px 0 20px; }
|
| 327 |
.app-heading h1 {
|
|
@@ -348,13 +390,11 @@ GLOBAL_CSS = """
|
|
| 348 |
.app-heading .chip svg {
|
| 349 |
width: 12px; height: 12px; opacity: 0.7;
|
| 350 |
}
|
| 351 |
-
|
| 352 |
/* ββ Controls column β subtle card to separate it from viewer ββ */
|
| 353 |
.controls-col > .block,
|
| 354 |
.controls-col > .form {
|
| 355 |
background: #fafafa !important;
|
| 356 |
}
|
| 357 |
-
|
| 358 |
/* ββ Camera viewer column label ββ */
|
| 359 |
.viewer-label {
|
| 360 |
font-size: 13px; font-weight: 600;
|
|
@@ -365,7 +405,6 @@ GLOBAL_CSS = """
|
|
| 365 |
.viewer-label .hint {
|
| 366 |
font-weight: 400; color: #9ca3af; font-size: 12px;
|
| 367 |
}
|
| 368 |
-
|
| 369 |
/* ββ Status display ββ replaces the plain textbox look */
|
| 370 |
.status-row {
|
| 371 |
display: flex; align-items: center; gap: 8px;
|
|
@@ -388,14 +427,12 @@ GLOBAL_CSS = """
|
|
| 388 |
border-color: #e5e7eb !important;
|
| 389 |
resize: none !important;
|
| 390 |
}
|
| 391 |
-
|
| 392 |
/* ββ Prompt box ββ */
|
| 393 |
.prompt-box textarea {
|
| 394 |
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", monospace !important;
|
| 395 |
font-size: 12px !important;
|
| 396 |
color: #6b7280 !important;
|
| 397 |
}
|
| 398 |
-
|
| 399 |
"""
|
| 400 |
|
| 401 |
GRADIO_THEME = gr.themes.Default()
|
|
@@ -404,7 +441,6 @@ GRADIO_THEME = gr.themes.Default()
|
|
| 404 |
# ββ App ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 405 |
def create_app():
|
| 406 |
|
| 407 |
-
# FIX: theme and css are now passed to launch(), not gr.Blocks()
|
| 408 |
with gr.Blocks(title="3D Camera View Generator") as demo:
|
| 409 |
|
| 410 |
gr.HTML("""
|
|
@@ -456,7 +492,6 @@ def create_app():
|
|
| 456 |
</div>
|
| 457 |
""")
|
| 458 |
|
| 459 |
-
# FIX: plain gr.HTML with dict value β no subclass, no inspect error
|
| 460 |
cam_view = gr.HTML(
|
| 461 |
value=DEFAULT_CAM_VALUE,
|
| 462 |
html_template=HTML_TEMPLATE,
|
|
@@ -571,9 +606,8 @@ def create_app():
|
|
| 571 |
|
| 572 |
if __name__ == "__main__":
|
| 573 |
demo = create_app()
|
| 574 |
-
# FIX: theme and css passed to launch() as required by Gradio 6.0
|
| 575 |
demo.launch(
|
| 576 |
debug=True,
|
| 577 |
theme=GRADIO_THEME,
|
| 578 |
css=GLOBAL_CSS,
|
| 579 |
-
)
|
|
|
|
| 3 |
- Qwen Image Edit + Lightning LoRA + Multi-Angle LoRA
|
| 4 |
- gr.HTML custom component (Gradio 6)
|
| 5 |
- ZeroGPU (HuggingFace Spaces)
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
import gradio as gr
|
|
|
|
| 124 |
{{#if value.img}}
|
| 125 |
<img class="cv-img" src="{{value.img}}">
|
| 126 |
{{else}}
|
| 127 |
+
<div class="cv-empty cv-dropzone" data-action="upload">
|
| 128 |
<svg class="cv-empty-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.25">
|
| 129 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
|
|
|
| 130 |
</svg>
|
| 131 |
+
<p class="cv-empty-title">Drop an image here</p>
|
| 132 |
+
<p class="cv-empty-sub">or click to browse</p>
|
| 133 |
+
<input type="file" class="cv-file-input" accept="image/*" />
|
| 134 |
</div>
|
| 135 |
{{/if}}
|
|
|
|
| 136 |
<div class="cv-hud">
|
| 137 |
<div class="cv-readout">
|
| 138 |
<span class="cv-lbl">Az</span><span class="cv-val">${value.az}°</span>
|
|
|
|
| 160 |
|
| 161 |
CSS_TEMPLATE = """
|
| 162 |
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
| 163 |
/* ββ Image well ββ dark neutral so images pop, same treatment as any
|
| 164 |
professional image editor / camera app preview area. Not a stylistic
|
| 165 |
choice but a functional one: images render best against dark. */
|
|
|
|
| 171 |
overflow: hidden;
|
| 172 |
display: flex; align-items: center; justify-content: center;
|
| 173 |
}
|
|
|
|
| 174 |
.cv-img {
|
| 175 |
max-width: 100%; max-height: 100%;
|
| 176 |
object-fit: contain; display: block;
|
| 177 |
}
|
|
|
|
| 178 |
/* empty state */
|
| 179 |
.cv-empty {
|
| 180 |
text-align: center; user-select: none;
|
|
|
|
| 192 |
font-size: 13px; max-width: 230px; line-height: 1.65;
|
| 193 |
color: rgba(255,255,255,0.25);
|
| 194 |
}
|
| 195 |
+
/* dropzone */
|
| 196 |
+
.cv-dropzone {
|
| 197 |
+
cursor: pointer;
|
| 198 |
+
border: 2px dashed rgba(255,255,255,0.15);
|
| 199 |
+
border-radius: 12px;
|
| 200 |
+
padding: 40px 24px;
|
| 201 |
+
transition: border-color 0.2s, background 0.2s;
|
| 202 |
+
}
|
| 203 |
+
.cv-dropzone:hover, .cv-dropzone.cv-drag-over {
|
| 204 |
+
border-color: #f97316;
|
| 205 |
+
background: rgba(249,115,22,0.06);
|
| 206 |
+
}
|
| 207 |
+
.cv-dropzone.cv-drag-over .cv-empty-icon {
|
| 208 |
+
color: #f97316;
|
| 209 |
+
}
|
| 210 |
+
.cv-file-input {
|
| 211 |
+
position: absolute;
|
| 212 |
+
width: 0; height: 0;
|
| 213 |
+
opacity: 0; pointer-events: none;
|
| 214 |
+
}
|
| 215 |
/* HUD β fades in on hover via CSS, no JS needed */
|
| 216 |
.cv-hud {
|
| 217 |
position: absolute; bottom: 16px; right: 16px;
|
|
|
|
| 219 |
opacity: 0; transition: opacity 0.16s ease; pointer-events: auto;
|
| 220 |
}
|
| 221 |
.cv-wrap:hover .cv-hud { opacity: 1; }
|
|
|
|
| 222 |
/* coordinate readout β white card floating over image */
|
| 223 |
.cv-readout {
|
| 224 |
display: flex; align-items: center; gap: 8px;
|
|
|
|
| 230 |
.cv-lbl { color: #9ca3af; font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; }
|
| 231 |
.cv-val { color: #111827; font-weight: 600; font-variant-numeric: tabular-nums; }
|
| 232 |
.cv-sep { color: #d1d5db; margin: 0 2px; }
|
|
|
|
| 233 |
/* controls panel β white card, same treatment as readout */
|
| 234 |
.cv-controls {
|
| 235 |
display: flex; align-items: center; gap: 8px;
|
|
|
|
| 237 |
border-radius: 10px; padding: 8px 10px;
|
| 238 |
box-shadow: 0 2px 12px rgba(0,0,0,0.25);
|
| 239 |
}
|
|
|
|
| 240 |
/* d-pad */
|
| 241 |
.cv-dpad {
|
| 242 |
display: grid;
|
|
|
|
| 259 |
transform: scale(1.1);
|
| 260 |
}
|
| 261 |
.cv-btn:active { transform: scale(0.92); background: #ffedd5; }
|
|
|
|
| 262 |
.cv-up { grid-column:2; grid-row:1; }
|
| 263 |
.cv-left { grid-column:1; grid-row:2; }
|
| 264 |
.cv-dot {
|
|
|
|
| 268 |
}
|
| 269 |
.cv-right { grid-column:3; grid-row:2; }
|
| 270 |
.cv-down { grid-column:2; grid-row:3; }
|
|
|
|
| 271 |
/* zoom column */
|
| 272 |
.cv-zoom { display: flex; flex-direction: column; gap: 3px; }
|
| 273 |
.cv-zbtn {
|
|
|
|
| 288 |
|
| 289 |
JS_ON_LOAD = """
|
| 290 |
const DIST_STEPS = [0.6, 1.0, 1.8];
|
|
|
|
| 291 |
function snapDist(d) {
|
| 292 |
return DIST_STEPS.reduce((p, c) => Math.abs(c - d) < Math.abs(p - d) ? c : p);
|
| 293 |
}
|
|
|
|
| 295 |
const idx = DIST_STEPS.indexOf(snapDist(Number(d)));
|
| 296 |
return DIST_STEPS[Math.max(0, Math.min(DIST_STEPS.length - 1, idx + dir))];
|
| 297 |
}
|
| 298 |
+
// --- Image upload (drag-drop + click-to-browse) ---
|
| 299 |
+
function loadImageFile(file) {
|
| 300 |
+
if (!file || !file.type.startsWith('image/')) return;
|
| 301 |
+
const reader = new FileReader();
|
| 302 |
+
reader.onload = function(e) {
|
| 303 |
+
const v = Object.assign({}, props.value);
|
| 304 |
+
props.value = { ...v, img: e.target.result };
|
| 305 |
+
};
|
| 306 |
+
reader.readAsDataURL(file);
|
| 307 |
+
}
|
| 308 |
+
element.addEventListener('dragover', function(e) {
|
| 309 |
+
e.preventDefault();
|
| 310 |
+
const dz = element.querySelector('.cv-dropzone');
|
| 311 |
+
if (dz) dz.classList.add('cv-drag-over');
|
| 312 |
+
});
|
| 313 |
+
element.addEventListener('dragleave', function(e) {
|
| 314 |
+
const dz = element.querySelector('.cv-dropzone');
|
| 315 |
+
if (dz) dz.classList.remove('cv-drag-over');
|
| 316 |
+
});
|
| 317 |
+
element.addEventListener('drop', function(e) {
|
| 318 |
+
e.preventDefault();
|
| 319 |
+
const dz = element.querySelector('.cv-dropzone');
|
| 320 |
+
if (dz) dz.classList.remove('cv-drag-over');
|
| 321 |
+
if (e.dataTransfer && e.dataTransfer.files.length) {
|
| 322 |
+
loadImageFile(e.dataTransfer.files[0]);
|
| 323 |
+
}
|
| 324 |
+
});
|
| 325 |
+
element.addEventListener('change', function(e) {
|
| 326 |
+
if (e.target.classList.contains('cv-file-input')) {
|
| 327 |
+
loadImageFile(e.target.files[0]);
|
| 328 |
+
}
|
| 329 |
+
});
|
| 330 |
// Delegated click listener β attached once, survives template re-renders.
|
| 331 |
element.addEventListener('click', function(e) {
|
| 332 |
+
// Handle dropzone click -> open file picker
|
| 333 |
+
const dz = e.target.closest('[data-action="upload"]');
|
| 334 |
+
if (dz) {
|
| 335 |
+
const fi = element.querySelector('.cv-file-input');
|
| 336 |
+
if (fi) fi.click();
|
| 337 |
+
return;
|
| 338 |
+
}
|
| 339 |
const btn = e.target.closest('[data-action]');
|
| 340 |
if (!btn) return;
|
|
|
|
| 341 |
const v = Object.assign({}, props.value);
|
| 342 |
let az = Number(v.az) || 0;
|
| 343 |
let el = Number(v.el) || 0;
|
| 344 |
let dist = Number(v.dist) || 1.0;
|
|
|
|
| 345 |
switch (btn.dataset.action) {
|
| 346 |
case 'az-minus': az = (az - 45 + 360) % 360; break;
|
| 347 |
case 'az-plus': az = (az + 45) % 360; break;
|
|
|
|
| 350 |
case 'dist-minus': dist = shiftDist(dist, -1); break;
|
| 351 |
case 'dist-plus': dist = shiftDist(dist, +1); break;
|
| 352 |
}
|
|
|
|
| 353 |
props.value = { ...v, az, el, dist };
|
| 354 |
trigger('submit');
|
| 355 |
});
|
|
|
|
| 364 |
.gradio-container .row {
|
| 365 |
flex-wrap: nowrap !important;
|
| 366 |
}
|
|
|
|
| 367 |
/* ββ Header ββ */
|
| 368 |
.app-heading { padding: 28px 0 20px; }
|
| 369 |
.app-heading h1 {
|
|
|
|
| 390 |
.app-heading .chip svg {
|
| 391 |
width: 12px; height: 12px; opacity: 0.7;
|
| 392 |
}
|
|
|
|
| 393 |
/* ββ Controls column β subtle card to separate it from viewer ββ */
|
| 394 |
.controls-col > .block,
|
| 395 |
.controls-col > .form {
|
| 396 |
background: #fafafa !important;
|
| 397 |
}
|
|
|
|
| 398 |
/* ββ Camera viewer column label ββ */
|
| 399 |
.viewer-label {
|
| 400 |
font-size: 13px; font-weight: 600;
|
|
|
|
| 405 |
.viewer-label .hint {
|
| 406 |
font-weight: 400; color: #9ca3af; font-size: 12px;
|
| 407 |
}
|
|
|
|
| 408 |
/* ββ Status display ββ replaces the plain textbox look */
|
| 409 |
.status-row {
|
| 410 |
display: flex; align-items: center; gap: 8px;
|
|
|
|
| 427 |
border-color: #e5e7eb !important;
|
| 428 |
resize: none !important;
|
| 429 |
}
|
|
|
|
| 430 |
/* ββ Prompt box ββ */
|
| 431 |
.prompt-box textarea {
|
| 432 |
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", monospace !important;
|
| 433 |
font-size: 12px !important;
|
| 434 |
color: #6b7280 !important;
|
| 435 |
}
|
|
|
|
| 436 |
"""
|
| 437 |
|
| 438 |
GRADIO_THEME = gr.themes.Default()
|
|
|
|
| 441 |
# ββ App ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 442 |
def create_app():
|
| 443 |
|
|
|
|
| 444 |
with gr.Blocks(title="3D Camera View Generator") as demo:
|
| 445 |
|
| 446 |
gr.HTML("""
|
|
|
|
| 492 |
</div>
|
| 493 |
""")
|
| 494 |
|
|
|
|
| 495 |
cam_view = gr.HTML(
|
| 496 |
value=DEFAULT_CAM_VALUE,
|
| 497 |
html_template=HTML_TEMPLATE,
|
|
|
|
| 606 |
|
| 607 |
if __name__ == "__main__":
|
| 608 |
demo = create_app()
|
|
|
|
| 609 |
demo.launch(
|
| 610 |
debug=True,
|
| 611 |
theme=GRADIO_THEME,
|
| 612 |
css=GLOBAL_CSS,
|
| 613 |
+
)
|