- Add multi-stage CSS build that compiles Tailwind into app/static/tw.css
- Add Tailwind config with dark tokens (bg/nav/card) and purge globs
- Add assets/input.css (@tailwind base/components/utilities + small utilities)
- Replace Tailwind CDN + REMOVE Flowbite CSS (keep Flowbite JS only)
- New base_tailwind.html (top navbar, responsive container, {%- block scripts -%})
- Port pages to Tailwind look/feel with wider content column:
- index: single-column form + recent results, fullscreen spinner overlay, copy-UUID
- result: sticky jump list, Tailwind tables/badges, Suspicious Scripts/Forms sections
- viewer: Monaco-based code viewer in Tailwind card, actions (copy/wrap/raw)
- ssl_tls macro: rewritten with Tailwind (details/summary for raw JSON)
- Dockerfile: add css-builder stage and copy built tw.css into /app/app/static
- Remove Flowbite stylesheet to avoid overrides; Flowbite JS loaded with defer
BREAKING CHANGE:
Legacy CSS classes/components (.card, .badge, etc.) are replaced by Tailwind utilities.
All templates now expect tw.css to be served from /static.
144 lines
5.1 KiB
HTML
144 lines
5.1 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Source Viewer{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-xl md:max-w-3xl lg:max-w-5xl xl:max-w-6xl 2xl:max-w-7xl mx-auto space-y-4">
|
|
<section class="bg-card border border-gray-800 rounded-xl p-4">
|
|
<header class="flex items-center justify-between gap-3 flex-wrap">
|
|
<div>
|
|
<h2 class="text-lg font-semibold">Source Viewer</h2>
|
|
<div class="text-sm text-gray-400">
|
|
<strong>File:</strong> <span id="fileName" class="break-all">{{ filename }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button id="copyBtn"
|
|
class="inline-flex items-center rounded-lg px-3 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-sm">
|
|
Copy
|
|
</button>
|
|
<button id="wrapBtn"
|
|
class="inline-flex items-center rounded-lg px-3 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-sm">
|
|
Toggle wrap
|
|
</button>
|
|
<a id="openRaw" href="{{ raw_url }}" target="_blank" rel="noopener"
|
|
class="inline-flex items-center rounded-lg px-3 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-sm">
|
|
Open raw
|
|
</a>
|
|
<a id="downloadRaw" href="{{ raw_url }}" download
|
|
class="inline-flex items-center rounded-lg px-3 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-sm">
|
|
Download
|
|
</a>
|
|
</div>
|
|
</header>
|
|
|
|
<div id="viewerStatus" class="text-sm text-gray-400 mt-2 mb-3"></div>
|
|
|
|
<div id="editor" class="h-[72vh] border border-gray-800 rounded-lg"></div>
|
|
</section>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<!-- Monaco AMD loader (no integrity to avoid mismatch) -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/vs/loader.min.js"
|
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
|
|
<script>
|
|
(function () {
|
|
const RAW_URL = "{{ raw_url }}";
|
|
const FILENAME = "{{ filename }}";
|
|
const LANGUAGE = "{{ language|default('', true) }}";
|
|
|
|
const statusEl = document.getElementById('viewerStatus');
|
|
|
|
function extToLang(name) {
|
|
if (!name) return 'plaintext';
|
|
const m = name.toLowerCase().match(/\.([a-z0-9]+)$/);
|
|
const ext = m ? m[1] : '';
|
|
const map = {
|
|
js:'javascript', mjs:'javascript', cjs:'javascript',
|
|
ts:'typescript', json:'json',
|
|
html:'html', htm:'html', css:'css',
|
|
py:'python', sh:'shell', bash:'shell',
|
|
yml:'yaml', yaml:'yaml',
|
|
md:'markdown', txt:'plaintext', log:'plaintext'
|
|
};
|
|
return map[ext] || 'plaintext';
|
|
}
|
|
|
|
// Wait until the AMD loader defines window.require
|
|
function waitForRequire(msLeft = 5000) {
|
|
return new Promise((resolve, reject) => {
|
|
const t0 = performance.now();
|
|
(function poll() {
|
|
if (window.require && typeof window.require === 'function') return resolve();
|
|
if (performance.now() - t0 > msLeft) return reject(new Error('Monaco loader not available'));
|
|
setTimeout(poll, 25);
|
|
})();
|
|
});
|
|
}
|
|
|
|
function configureMonaco() {
|
|
// Point AMD loader at the CDN
|
|
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/vs' } });
|
|
// Worker bootstrap
|
|
window.MonacoEnvironment = {
|
|
getWorkerUrl: function () {
|
|
const base = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/';
|
|
const code = "self.MonacoEnvironment={baseUrl:'" + base + "'};importScripts('" + base + "vs/base/worker/workerMain.js');";
|
|
return 'data:text/javascript;charset=utf-8,' + encodeURIComponent(code);
|
|
}
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
statusEl.textContent = 'Loading file…';
|
|
await waitForRequire();
|
|
configureMonaco();
|
|
|
|
const resp = await fetch(RAW_URL, { cache: 'no-store' });
|
|
const text = await resp.text();
|
|
|
|
require(['vs/editor/editor.main'], function () {
|
|
const editor = monaco.editor.create(document.getElementById('editor'), {
|
|
value: text,
|
|
language: LANGUAGE || extToLang(FILENAME),
|
|
readOnly: true,
|
|
automaticLayout: true,
|
|
wordWrap: 'on',
|
|
minimap: { enabled: false },
|
|
scrollBeyondLastLine: false,
|
|
theme: 'vs-dark'
|
|
});
|
|
|
|
// Buttons
|
|
document.getElementById('copyBtn')?.addEventListener('click', async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(editor.getValue());
|
|
statusEl.textContent = 'Copied.';
|
|
} catch (e) {
|
|
statusEl.textContent = 'Copy failed: ' + e;
|
|
}
|
|
});
|
|
|
|
document.getElementById('wrapBtn')?.addEventListener('click', () => {
|
|
const opts = editor.getRawOptions();
|
|
editor.updateOptions({ wordWrap: opts.wordWrap === 'on' ? 'off' : 'on' });
|
|
});
|
|
|
|
// Status
|
|
const warn = resp.ok ? '' : `Warning: HTTP ${resp.status}`;
|
|
const empty = text.length ? '' : (warn ? ' · empty file' : 'Empty file');
|
|
statusEl.textContent = warn + (warn && empty ? '' : '') + (empty || '');
|
|
});
|
|
} catch (err) {
|
|
statusEl.textContent = 'Viewer error: ' + err.message;
|
|
}
|
|
}
|
|
|
|
main();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|