Added Rules preset (on top of format and style presets)

This commit is contained in:
2026-03-13 21:02:03 +01:00
parent 3105b72ac2
commit 3d1a391cba
7 changed files with 848 additions and 131 deletions

View File

@@ -0,0 +1,395 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DataGridFormattingManager — Visual Mockup</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/daisyui.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style>
body { font-family: ui-sans-serif, system-ui, sans-serif; }
/* ---- Manager layout ---- */
.manager-root {
display: grid;
grid-template-columns: 280px 1fr;
grid-template-rows: auto 1fr;
height: 520px;
border: 1px solid oklch(var(--b3));
border-radius: 0.75rem;
overflow: hidden;
}
/* ---- Menu bar (Menu control pattern) ---- */
.mf-menu {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.35rem 0.6rem;
background: oklch(var(--b2));
border-bottom: 1px solid oklch(var(--b3));
}
/* Icon button — mirrors mk.icon() output */
.mf-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 0.4rem;
cursor: pointer;
color: oklch(var(--bc) / 0.7);
transition: background 0.1s, color 0.1s;
position: relative;
}
.mf-icon-btn:hover { background: oklch(var(--b3)); color: oklch(var(--bc)); }
.mf-icon-btn.danger:hover { background: color-mix(in oklab, oklch(var(--er)) 15%, transparent); color: oklch(var(--er)); }
/* Tooltip — mirrors mk.icon(tooltip=...) */
.mf-icon-btn::after {
content: attr(data-tip);
position: absolute;
top: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: oklch(var(--n));
color: oklch(var(--nc));
font-size: 0.7rem;
white-space: nowrap;
padding: 0.2rem 0.5rem;
border-radius: 0.3rem;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
z-index: 10;
}
.mf-icon-btn:hover::after { opacity: 1; }
.menu-separator { width: 1px; height: 1.2rem; background: oklch(var(--b3)); margin: 0 0.15rem; }
/* ---- Preset list ---- */
.preset-list {
overflow-y: auto;
border-right: 1px solid oklch(var(--b3));
background: oklch(var(--b1));
}
.preset-item {
display: flex;
flex-direction: column;
gap: 0.15rem;
padding: 0.55rem 0.75rem;
cursor: pointer;
border-bottom: 1px solid oklch(var(--b2));
transition: background 0.1s;
}
.preset-item:hover { background: oklch(var(--b2)); }
.preset-item.active {
background: color-mix(in oklab, oklch(var(--p)) 10%, transparent);
border-left: 3px solid oklch(var(--p));
padding-left: calc(0.75rem - 3px);
}
.preset-name { font-size: 0.875rem; font-weight: 600; }
.preset-desc { font-size: 0.7rem; color: oklch(var(--bc) / 0.5); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.preset-badges { display: flex; gap: 0.25rem; margin-top: 0.2rem; }
/* ---- Editor panel ---- */
.editor-panel {
display: flex;
flex-direction: column;
overflow: hidden;
background: oklch(var(--b1));
}
.editor-meta {
padding: 0.6rem 1rem 0.5rem;
border-bottom: 1px solid oklch(var(--b3));
display: flex;
align-items: center;
gap: 0.75rem;
}
.dsl-area {
flex: 1;
padding: 0.75rem 1rem;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.dsl-label {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: oklch(var(--bc) / 0.4);
}
.dsl-editor {
flex: 1;
font-family: ui-monospace, 'Cascadia Code', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
resize: none;
padding: 0.75rem;
border: 1px solid oklch(var(--b3));
border-radius: 0.5rem;
background: oklch(var(--b2));
color: oklch(var(--bc));
outline: none;
transition: border-color 0.15s;
}
.dsl-editor:focus { border-color: oklch(var(--p)); }
/* ---- Code block (Python def) ---- */
.token-keyword { color: oklch(var(--p)); font-weight: 700; }
.token-builtin { color: oklch(var(--s)); font-weight: 600; }
.token-string { color: oklch(var(--su)); }
.token-number { color: oklch(var(--a)); }
.token-comment { color: oklch(var(--bc) / 0.4); font-style: italic; }
</style>
</head>
<body class="bg-base-200 p-6">
<!-- Page header -->
<div class="mb-4">
<h1 class="text-xl font-bold">DataGridFormattingManager</h1>
<p class="text-sm text-base-content/50 mt-0.5">
Named rule presets combining formatters, styles and conditions.
Reference them with <code class="bg-base-300 px-1 rounded text-xs">format("preset_name")</code>
or <code class="bg-base-300 px-1 rounded text-xs">style("preset_name")</code>.
</p>
</div>
<!-- Manager control -->
<div class="manager-root shadow-md">
<!-- Menu bar — Menu(conf=MenuConf(fixed_items=["New","Save","Rename","Delete"]), save_state=False) -->
<div class="mf-menu">
<!-- fixed_items rendered as mk.icon(command, tooltip=command.description) -->
<button class="mf-icon-btn" data-tip="New preset" onclick="newPreset()">
<!-- Fluent: Add -->
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
<button class="mf-icon-btn" data-tip="Save preset" onclick="savePreset()">
<!-- Fluent: Save -->
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
</button>
<button class="mf-icon-btn" data-tip="Rename preset" onclick="renamePreset()">
<!-- Fluent: Rename -->
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<div class="menu-separator"></div>
<button class="mf-icon-btn danger" data-tip="Delete preset" onclick="deletePreset()">
<!-- Fluent: Delete -->
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
</button>
</div>
<!-- Left: preset list -->
<div class="preset-list" id="preset-list"></div>
<!-- Right: editor -->
<div class="editor-panel">
<div class="editor-meta">
<div class="flex-1">
<div class="text-sm font-semibold" id="editor-title"></div>
<div class="text-xs text-base-content/45 mt-0.5" id="editor-desc">Select a preset to edit</div>
</div>
<div class="flex gap-1" id="editor-badges"></div>
</div>
<div class="dsl-area">
<div class="dsl-label">Rules — DSL</div>
<textarea class="dsl-editor" id="dsl-editor" spellcheck="false"
placeholder="No preset selected."></textarea>
</div>
</div>
</div>
<!-- DEFAULT_RULE_PRESETS example -->
<div class="mt-6 rounded-xl border border-base-300 bg-base-100 overflow-hidden shadow">
<div class="px-4 py-2 bg-base-200 border-b border-base-300 text-xs font-bold uppercase tracking-wide text-base-content/50">
DEFAULT_RULE_PRESETS — core/formatting/presets.py
</div>
<pre class="p-4 text-xs font-mono overflow-x-auto leading-relaxed text-base-content"><span class="token-comment"># name + description + list of complete FormatRule descriptors.
# Appears in format() suggestions if at least one rule has a formatter.
# Appears in style() suggestions if at least one rule has a style.</span>
DEFAULT_RULE_PRESETS = {
<span class="token-string">"accounting"</span>: {
<span class="token-string">"description"</span>: <span class="token-string">"Negatives in parentheses (red), positives plain"</span>,
<span class="token-string">"rules"</span>: [
{
<span class="token-string">"condition"</span>: {<span class="token-string">"operator"</span>: <span class="token-string">"&lt;"</span>, <span class="token-string">"value"</span>: <span class="token-number">0</span>},
<span class="token-string">"formatter"</span>: {<span class="token-string">"type"</span>: <span class="token-string">"number"</span>, <span class="token-string">"precision"</span>: <span class="token-number">0</span>,
<span class="token-string">"prefix"</span>: <span class="token-string">"("</span>, <span class="token-string">"suffix"</span>: <span class="token-string">")"</span>,
<span class="token-string">"absolute"</span>: <span class="token-keyword">True</span>, <span class="token-string">"thousands_sep"</span>: <span class="token-string">" "</span>},
<span class="token-string">"style"</span>: {<span class="token-string">"preset"</span>: <span class="token-string">"error"</span>},
},
{
<span class="token-string">"condition"</span>: {<span class="token-string">"operator"</span>: <span class="token-string">"&gt;"</span>, <span class="token-string">"value"</span>: <span class="token-number">0</span>},
<span class="token-string">"formatter"</span>: {<span class="token-string">"type"</span>: <span class="token-string">"number"</span>, <span class="token-string">"precision"</span>: <span class="token-number">0</span>,
<span class="token-string">"thousands_sep"</span>: <span class="token-string">" "</span>},
},
],
},
<span class="token-string">"traffic_light"</span>: {
<span class="token-string">"description"</span>: <span class="token-string">"Red / yellow / green style based on sign"</span>,
<span class="token-string">"rules"</span>: [
{<span class="token-string">"condition"</span>: {<span class="token-string">"operator"</span>: <span class="token-string">"&lt;"</span>, <span class="token-string">"value"</span>: <span class="token-number">0</span>}, <span class="token-string">"style"</span>: {<span class="token-string">"preset"</span>: <span class="token-string">"error"</span>}},
{<span class="token-string">"condition"</span>: {<span class="token-string">"operator"</span>: <span class="token-string">"=="</span>, <span class="token-string">"value"</span>: <span class="token-number">0</span>}, <span class="token-string">"style"</span>: {<span class="token-string">"preset"</span>: <span class="token-string">"warning"</span>}},
{<span class="token-string">"condition"</span>: {<span class="token-string">"operator"</span>: <span class="token-string">"&gt;"</span>, <span class="token-string">"value"</span>: <span class="token-number">0</span>}, <span class="token-string">"style"</span>: {<span class="token-string">"preset"</span>: <span class="token-string">"success"</span>}},
],
},
<span class="token-string">"budget_variance"</span>: {
<span class="token-string">"description"</span>: <span class="token-string">"% variance: negative=error, over 10%=warning, else plain"</span>,
<span class="token-string">"rules"</span>: [
{
<span class="token-string">"condition"</span>: {<span class="token-string">"operator"</span>: <span class="token-string">"&lt;"</span>, <span class="token-string">"value"</span>: <span class="token-number">0</span>},
<span class="token-string">"formatter"</span>: {<span class="token-string">"type"</span>: <span class="token-string">"number"</span>, <span class="token-string">"precision"</span>: <span class="token-number">1</span>, <span class="token-string">"suffix"</span>: <span class="token-string">"%"</span>, <span class="token-string">"multiplier"</span>: <span class="token-number">100</span>},
<span class="token-string">"style"</span>: {<span class="token-string">"preset"</span>: <span class="token-string">"error"</span>},
},
{
<span class="token-string">"condition"</span>: {<span class="token-string">"operator"</span>: <span class="token-string">"&gt;"</span>, <span class="token-string">"value"</span>: <span class="token-number">0.1</span>},
<span class="token-string">"formatter"</span>: {<span class="token-string">"type"</span>: <span class="token-string">"number"</span>, <span class="token-string">"precision"</span>: <span class="token-number">1</span>, <span class="token-string">"suffix"</span>: <span class="token-string">"%"</span>, <span class="token-string">"multiplier"</span>: <span class="token-number">100</span>},
<span class="token-string">"style"</span>: {<span class="token-string">"preset"</span>: <span class="token-string">"warning"</span>},
},
{
<span class="token-string">"formatter"</span>: {<span class="token-string">"type"</span>: <span class="token-string">"number"</span>, <span class="token-string">"precision"</span>: <span class="token-number">1</span>, <span class="token-string">"suffix"</span>: <span class="token-string">"%"</span>, <span class="token-string">"multiplier"</span>: <span class="token-number">100</span>},
},
],
},
}</pre>
</div>
<script>
const presets = [
{
id: "accounting", name: "accounting",
description: "Negatives in parentheses (red), positives plain",
hasFormatter: true, hasStyle: true,
dsl:
`format.number(precision=0, prefix="(", suffix=")", absolute=True, thousands_sep=" ") style("error") if value < 0
format.number(precision=0, thousands_sep=" ") if value > 0`,
},
{
id: "traffic_light", name: "traffic_light",
description: "Red / yellow / green style based on sign",
hasFormatter: false, hasStyle: true,
dsl:
`style("error") if value < 0
style("warning") if value == 0
style("success") if value > 0`,
},
{
id: "budget_variance", name: "budget_variance",
description: "% variance: negative=error, over 10%=warning, else plain",
hasFormatter: true, hasStyle: true,
dsl:
`format.number(precision=1, suffix="%", multiplier=100) style("error") if value < 0
format.number(precision=1, suffix="%", multiplier=100) style("warning") if value > 0.1
format.number(precision=1, suffix="%", multiplier=100)`,
},
];
let activeId = null;
function renderList() {
const list = document.getElementById("preset-list");
list.innerHTML = "";
presets.forEach(p => {
const item = document.createElement("div");
item.className = "preset-item" + (p.id === activeId ? " active" : "");
item.onclick = () => selectPreset(p.id);
const badges = [];
if (p.hasFormatter) badges.push(`<span class="badge badge-xs badge-secondary">Format</span>`);
if (p.hasStyle) badges.push(`<span class="badge badge-xs badge-primary">Style</span>`);
item.innerHTML = `
<div class="preset-name">${p.name}</div>
<div class="preset-desc">${p.description}</div>
<div class="preset-badges">${badges.join("")}</div>
`;
list.appendChild(item);
});
}
function selectPreset(id) {
activeId = id;
const p = presets.find(x => x.id === id);
document.getElementById("editor-title").textContent = p.name;
document.getElementById("editor-desc").textContent = p.description;
document.getElementById("dsl-editor").value = p.dsl;
const badges = [];
if (p.hasFormatter) badges.push(`<span class="badge badge-secondary">format()</span>`);
if (p.hasStyle) badges.push(`<span class="badge badge-primary">style()</span>`);
document.getElementById("editor-badges").innerHTML = badges.join("");
renderList();
}
function savePreset() {
if (!activeId) return;
const p = presets.find(x => x.id === activeId);
p.dsl = document.getElementById("dsl-editor").value;
p.hasFormatter = p.dsl.includes("format");
p.hasStyle = p.dsl.includes("style");
renderList();
showToast("Preset saved.");
}
function newPreset() {
const name = prompt("Preset name:");
if (!name?.trim()) return;
const id = name.trim().toLowerCase().replace(/\s+/g, "_");
if (presets.find(x => x.id === id)) { alert("Name already exists."); return; }
presets.push({ id, name: id, description: "New rule preset", hasFormatter: false, hasStyle: false, dsl: "" });
renderList();
selectPreset(id);
}
function renamePreset() {
if (!activeId) return;
const p = presets.find(x => x.id === activeId);
const name = prompt("New name:", p.name);
if (!name?.trim()) return;
p.name = name.trim();
p.id = p.name.toLowerCase().replace(/\s+/g, "_");
activeId = p.id;
renderList();
document.getElementById("editor-title").textContent = p.name;
}
function deletePreset() {
if (!activeId) return;
if (!confirm(`Delete "${activeId}"?`)) return;
presets.splice(presets.findIndex(x => x.id === activeId), 1);
activeId = null;
document.getElementById("editor-title").textContent = "—";
document.getElementById("editor-desc").textContent = "Select a preset to edit";
document.getElementById("dsl-editor").value = "";
document.getElementById("editor-badges").innerHTML = "";
renderList();
}
function showToast(msg) {
const t = document.createElement("div");
t.className = "toast toast-end toast-bottom z-50";
t.innerHTML = `<div class="alert alert-success text-sm py-2 px-4">${msg}</div>`;
document.body.appendChild(t);
setTimeout(() => t.remove(), 2000);
}
renderList();
selectPreset("accounting");
</script>
</body>
</html>