use crate::wasm::grid;
use horrorshow::prelude::*;
use horrorshow::Raw;
unsafe extern "C" {
fn mount_view(ptr: *const u8, len: usize);
}
/// The Tachyons stylesheet, embedded into the binary at compile time so the
/// injected markup carries its own styles and needs nothing from the host page.
const STYLES: &str = include_str!("../t.css");
/// Flyout-specific chrome that Tachyons can't express cleanly: the fixed
/// launcher/panel positioning, exact icon size, scroll behaviour, the
/// open/closed toggle (driven by the `hecatomb-open` class, added in JS) and the
/// colour theme. All rules are scoped to `.hecatomb-*` so nothing leaks into the
/// host page.
///
/// Colour palette — a warm "Daily Stoic" parchment scheme. The functional
/// element classes (`.hecatomb-rescan`, `.heca-generate`, ...) double as the
/// styling hooks, so the markup carries only layout utilities, not colours.
///
/// | Role | Hex | Used for |
/// |------------------------------|-----------|---------------------------------------------|
/// | Ink (primary text) | `#241f17` | body text, headings, primary-button labels |
/// | Muted text | `#7a7159` | captions, labels, close glyph, Back button |
/// | Parchment (panel background) | `#f3ecdc` | the flyout panel surface |
/// | Paper (card background) | `#fbf6ea` | detail card + list rows (`.heca-grid-row`) |
/// | Field (input background) | `#fffdf7` | the row-count input + search box |
/// | Border tan | `#d8ccb0` | panel border, input border, outline buttons |
/// | Border soft | `#e6dcc6` | card borders |
/// | Amber (accent) | `#d29a34` | launcher + primary buttons |
/// | Amber hover | `#e0a83f` | launcher + primary button hover |
/// | Ink card (badge background) | `#3a342a` | row-model badge |
/// | Parchment text on badge | `#efe6d2` | row-model badge label |
const FLYOUT_CSS: &str = r#"
.hecatomb-flyout {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 2147483647;
display: flex;
flex-direction: column;
align-items: flex-end;
color: #241f17;
}
.hecatomb-launcher {
width: 48px;
height: 48px;
border: 0;
border-radius: 12px;
background: #d29a34;
color: #241f17;
font-size: 20px;
font-weight: 800;
letter-spacing: 0.5px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.hecatomb-launcher:hover { background: #e0a83f; }
.hecatomb-panel {
display: none;
position: fixed;
left: 0;
right: 0;
bottom: 76px;
max-height: 70vh;
overflow: auto;
background: #f3ecdc;
border: 1px solid #d8ccb0;
color: #241f17;
}
.hecatomb-flyout.hecatomb-open .hecatomb-panel {
display: block;
animation: hecatomb-pop 0.12s ease-out;
}
.hecatomb-flyout .heca-muted { color: #7a7159; }
.hecatomb-flyout .heca-card {
background: #fbf6ea;
border-color: #e6dcc6;
}
.hecatomb-flyout .heca-badge {
background: #3a342a;
color: #efe6d2;
}
.hecatomb-flyout .heca-count,
.hecatomb-flyout .heca-search {
background: #fffdf7;
border: 1px solid #d8ccb0;
color: #241f17;
}
.hecatomb-flyout .heca-search {
font: inherit;
box-sizing: border-box;
}
.hecatomb-flyout .heca-search::placeholder { color: #a89e84; }
.hecatomb-flyout .heca-grid-row {
font: inherit;
background: #fbf6ea;
border-color: #e6dcc6;
color: #241f17;
}
.hecatomb-flyout .heca-grid-row:hover { background: #f0e7d3; }
.hecatomb-flyout .hecatomb-rescan,
.hecatomb-flyout .heca-generate {
background: #d29a34;
color: #241f17;
font-weight: 700;
}
.hecatomb-flyout .hecatomb-rescan:hover,
.hecatomb-flyout .heca-generate:hover { background: #e0a83f; }
.hecatomb-flyout .heca-save,
.hecatomb-flyout .heca-load {
background: transparent;
border: 1px solid #d8ccb0;
color: #5c5340;
}
.hecatomb-flyout .heca-save:hover,
.hecatomb-flyout .heca-load:hover { background: #ece2cc; }
.hecatomb-flyout .heca-back {
font: inherit;
background: transparent;
color: #7a7159;
}
.hecatomb-flyout .heca-back:hover { color: #241f17; }
.hecatomb-close {
width: 1.75rem;
height: 1.75rem;
line-height: 1;
background: transparent;
color: #7a7159;
}
.hecatomb-close:hover { color: #241f17; }
@keyframes hecatomb-pop {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
"#;
/// Render the Hecatomb UI into every `.hecatomb` mount point on the page.
///
/// The actual DOM insertion happens on the JS side (see `mount_view` in
/// `template.js`); here we build the markup with horrorshow and hand it over.
pub fn render() {
let grids = grid::find();
let markup = build(&grids);
unsafe {
mount_view(markup.as_ptr(), markup.len());
}
}
/// Build the styled markup for the flyout. A small square launcher icon floats
/// at the bottom-right of the page; pressing it opens the panel.
///
/// The panel is a two-level master/detail: the **list view** shows every
/// discovered grid as a searchable, clickable row; selecting one swaps to that
/// grid's **detail view**, which carries the controls to generate synthetic data
/// and save/load a per-table config file. Both views are emitted here on every
/// render; which one is visible (and the search text) is JS state re-applied
/// after each mount — see `applyViewState`/`applySearch` in `template.js`. The
/// launcher is fixed-position, so wherever the `.hecatomb` mount point sits in
/// the host DOM is irrelevant.
fn build(grids: &[grid::Grid]) -> String {
(html! {
// Inlined so the injected content can be styled with Tachyons.
style : Raw(STYLES);
style : Raw(FLYOUT_CSS);
div(class="hecatomb-flyout sans-serif") {
// Collapsible panel, hidden until `.hecatomb-open` is toggled on.
div(class="hecatomb-panel br3 shadow-4") {
div(class="pa3") {
div(class="flex items-center justify-between mb2") {
h4(class="f4 mv0") : "Hecatomb";
div(class="flex items-center") {
button(class="hecatomb-rescan f6 bn br2 ph3 pv2 pointer grow mr2") : "Rescan";
button(
class="hecatomb-toggle hecatomb-close f4 bn br2 pointer grow",
title="Close"
) : Raw("×");
}
}
// List view: a searchable, clickable index of grids. JS hides
// it while a detail view is open.
div(class="heca-list") {
@ if grids.is_empty() {
p(class="i heca-muted mt0") : "No AG Grid tables found on this page.";
} else {
input(
class="heca-search w-100 pa2 br2 mb2",
type="search",
placeholder="Search tables\u{2026}",
autocomplete="off"
);
p(class="f6 heca-muted mt0 mb2") {
: format_args!("{} AG Grid table(s) found", grids.len());
}
ul(class="list pl0 mv0") {
@ for (index, grid) in grids.iter().enumerate() {
li(class="mb2") {
button(
class="heca-grid-row w-100 tl pa2 ba br2 pointer flex items-center justify-between",
data-index=index
) {
span(class="flex items-center") {
span(class="b") : grid.label.as_str();
span(class="f7 ml2 ph2 pv1 br2 heca-badge") : grid.row_model.as_str();
}
span(class="heca-chevron f3 heca-muted ml2") : Raw("›");
}
}
}
}
p(class="heca-empty i heca-muted f6 mt2", style="display:none") {
: "No tables match your search.";
}
}
}
// One detail view per grid, all hidden until JS reveals the
// selected one. Holds the generate / save / load actions.
@ for (index, grid) in grids.iter().enumerate() {
div(class="heca-detail", data-index=index, style="display:none") {
button(class="heca-back f6 bn br2 ph2 pv1 pointer mb3") : Raw("‹ Back to list");
div(class="flex items-center justify-between mb1") {
span(class="b f5") : grid.label.as_str();
span(class="f7 ml2 ph2 pv1 br2 heca-badge") : grid.row_model.as_str();
}
div(class="mt3 pa3 ba br2 heca-card") {
div(class="flex items-center flex-wrap") {
label(class="f6 heca-muted mr2") : "rows";
input(
class="heca-count w3 mr2 pa1 br2",
type="number",
min="1",
value=grid.default_count,
data-index=index
);
button(
class="heca-generate f6 bn br2 ph3 pv2 mr2 pointer grow",
data-index=index
) : "Generate";
button(
class="heca-save f6 br2 ph3 pv2 mr2 pointer grow",
data-index=index
) : "Save config";
button(
class="heca-load f6 br2 ph3 pv2 pointer grow",
data-index=index
) : "Load config";
}
}
}
}
}
}
// Always-visible square launcher; toggles the panel open/closed.
button(class="hecatomb-toggle hecatomb-launcher shadow-3", title="Hecatomb") : "HB";
}
})
.into_string()
.unwrap_or_default()
}