Final fixes to CSS.

This commit is contained in:
Yaro Kasear 2026-01-08 11:58:53 -06:00
parent c51ef38d99
commit 6d711474df
4 changed files with 234 additions and 246 deletions

View file

@ -1,134 +1,33 @@
:root { :root { --tb-h: 34px; }
--tb-h: 34px;
}
.grid-widget { /* =========================================================
container-type: inline-size; GRID WIDGET (editor uses container queries, viewer does not)
min-width: 375px; ========================================================= */
height: 100%;
display: flex;
flex-direction: column;
}
.grid-widget .grid-wrap { /* -------------------------
flex: 1 1 auto; Shared basics (both modes)
width: 100%; ------------------------- */
position: relative;
overflow: hidden;
min-height: 375px;
z-index: 0;
}
.grid-widget [data-toolbar].toolbar { .grid-widget { /* no container-type here */ }
display: grid !important;
grid-template-rows: auto auto;
align-content: start;
gap: .5rem;
overflow: visible;
}
.grid-widget [data-toolbar] .toolbar-row {
display: flex;
align-items: center;
gap: .5rem;
min-width: 0;
flex-wrap: nowrap;
}
.grid-widget [data-toolbar] .toolbar-row--primary,
.grid-widget [data-toolbar] .toolbar-row--secondary {
overflow-x: auto;
overflow-y: hidden;
}
.grid-widget [data-toolbar] .toolbar-row--secondary {
opacity: .95;
}
.grid-widget[data-mode="viewer"] {
display: inline-block;
height: auto;
min-width: 0;
}
.grid-widget[data-mode="viewer"] .grid-wrap {
flex: 0 0 auto;
min-height: 0;
width: fit-content;
overflow: visible;
}
.grid-widget[data-mode="viewer"] [data-grid]{
position: relative;
width: auto;
height: auto;
inset: auto;
cursor: default;
}
/* WIDE: 1 row */
@container (min-width: 750px) {
.grid-widget [data-toolbar].toolbar {
display: flex !important;
flex-wrap: nowrap;
align-items: center;
gap: .5rem;
overflow-x: auto;
overflow-y: hidden;
}
.grid-widget [data-toolbar] .toolbar-row {
display: contents;
}
.grid-widget [data-toolbar] .toolbar-row--primary,
.grid-widget [data-toolbar] .toolbar-row--secondary {
overflow: visible;
}
}
/* drawing stack */
.grid-widget [data-grid] { .grid-widget [data-grid] {
position: absolute; position: relative;
cursor: crosshair; margin-inline: auto;
width: 100%;
height: 100%;
touch-action: none;
margin: 0 auto;
max-width: 100%;
z-index: 0;
inset: 0;
} }
/* Overlay elements */
.grid-widget [data-canvas], .grid-widget [data-canvas],
.grid-widget [data-coords], .grid-widget [data-dot],
.grid-widget [data-dot] { .grid-widget [data-coords] { position: absolute; }
position: absolute;
}
.grid-widget [data-toolbar]::-webkit-scrollbar {
height: 8px;
z-index: 10;
}
.grid-widget [data-grid-wrap] {
min-width: 0;
}
.grid-widget [data-coords] {
bottom: 10px;
pointer-events: none;
left: 10px;
}
.grid-widget [data-canvas]{ .grid-widget [data-canvas]{
inset: 0;
width: 100%;
height: 100%;
display: block;
z-index: 1; z-index: 1;
pointer-events: none; pointer-events: none;
inset: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: block;
} }
.grid-widget [data-dot]{ .grid-widget [data-dot]{
@ -137,122 +36,189 @@
pointer-events: none; pointer-events: none;
} }
.grid-widget .toolbar-range { .grid-widget [data-coords]{
width: 120px; bottom: 10px;
margin: 0; left: 10px;
flex: 0 0 120px; pointer-events: none;
} }
.grid-widget .dropdown-menu { /* -------------------------
min-width: 200px; Toolbar styling
padding: .5rem .75rem; ------------------------- */
border-radius: .75rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, .12); .grid-widget [data-toolbar].toolbar{
position: absolute; display: grid !important;
z-index: 1000 !important; grid-template-rows: auto auto;
pointer-events: auto; align-content: start;
gap: 0.5rem;
overflow: visible;
} }
.grid-widget .dropdown-menu>*:first-child { .grid-widget [data-toolbar] .toolbar-row{
margin-top: 0; display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
flex-wrap: nowrap;
} }
.grid-widget .dropdown-menu>*:last-child { .grid-widget [data-toolbar] .toolbar-row--primary,
margin-bottom: 0; .grid-widget [data-toolbar] .toolbar-row--secondary{
overflow-x: auto;
overflow-y: hidden;
} }
.grid-widget .dropdown-menu .form-range { .grid-widget [data-toolbar] .toolbar-row--secondary{ opacity: 0.95; }
width: 100%;
margin: 0; /* container query only matters in editor (set below) */
@container (min-width: 750px){
.grid-widget [data-toolbar].toolbar{
display: flex !important;
flex-wrap: nowrap;
align-items: center;
gap: 0.5rem;
overflow-x: auto;
overflow-y: hidden;
} }
.grid-widget [data-toolbar] .badge, .grid-widget [data-toolbar] .toolbar-row{ display: contents; }
.grid-widget [data-toolbar] .input-group-text {
white-space: nowrap; .grid-widget [data-toolbar] .toolbar-row--primary,
.grid-widget [data-toolbar] .toolbar-row--secondary{ overflow: visible; }
} }
.grid-widget [data-toolbar]>* { .grid-widget [data-toolbar]::-webkit-scrollbar{ height: 8px; }
align-self: center;
}
.grid-widget [data-toolbar] .vr {
align-self: stretch;
}
.grid-widget [data-toolbar] .btn-group,
.grid-widget [data-toolbar] .dropdown,
.grid-widget [data-toolbar] .vr {
flex: 0 0 auto;
}
.grid-widget .toolbar-group{ .grid-widget .toolbar-group{
display: flex; display: flex;
align-items: center; align-items: center;
gap: .25rem; gap: 0.25rem;
padding: .25rem; padding: 0.25rem;
border: 1px solid rgba(0, 0, 0, .08); border: 1px solid rgba(0,0,0,0.08);
border-radius: .5rem; border-radius: 0.5rem;
background: rgba(0, 0, 0, .02); background: rgba(0,0,0,0.02);
} }
.grid-widget .btn, .grid-widget .btn,
.grid-widget .form-control, .grid-widget .form-control,
.grid-widget .badge { .grid-widget .badge{ height: var(--tb-h); }
height: var(--tb-h);
} .grid-widget [data-toolbar] .badge,
.grid-widget [data-toolbar] .input-group-text{ white-space: nowrap; }
.grid-widget .toolbar .btn{ .grid-widget .toolbar .btn{
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 .5rem; padding: 0 0.5rem;
} }
.grid-widget .toolbar .form-control-color { .grid-widget .toolbar .form-control-color{ width: var(--tb-h); padding: 0; }
width: var(--tb-h);
padding: 0;
}
.grid-widget .tb-btn { .grid-widget .tb-btn{ flex-direction: column; gap: 2px; line-height: 1; }
flex-direction: column; .grid-widget .tb-btn small{ font-size: 11px; opacity: 0.75; }
gap: 2px;
line-height: 1;
}
.grid-widget .tb-btn small { .grid-widget .dropdown-toggle::after{ display: none; }
font-size: 11px;
opacity: .75;
}
.grid-widget .dropdown-toggle::after {
display: none;
}
.grid-widget .toolbar .btn-group .btn {
border-radius: 0;
}
.grid-widget .toolbar .btn-group .btn{ border-radius: 0; }
.grid-widget .toolbar .btn-group .btn:first-child{ .grid-widget .toolbar .btn-group .btn:first-child{
border-top-left-radius: .5rem; border-top-left-radius: 0.5rem;
border-bottom-left-radius: .5rem; border-bottom-left-radius: 0.5rem;
} }
.grid-widget .toolbar .btn-group .btn:last-child{ .grid-widget .toolbar .btn-group .btn:last-child{
border-top-right-radius: .5rem; border-top-right-radius: 0.5rem;
border-bottom-right-radius: .5rem; border-bottom-right-radius: 0.5rem;
} }
.grid-widget .btn-check:checked + .btn{ .grid-widget .btn-check:checked + .btn{
background: rgba(0, 0, 0, .08); background: rgba(0,0,0,0.08);
border-color: rgba(0, 0, 0, .18); border-color: rgba(0,0,0,0.18);
} }
.grid-widget [data-toolbar], .grid-widget .dropdown-menu{
.grid-widget [data-grid] { min-width: 200px;
margin-inline: auto; padding: 0.5rem 0.75rem;
border-radius: 0.75rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.12);
position: absolute;
z-index: 1000;
pointer-events: auto;
} }
.grid-widget [data-toolbar] { .grid-widget .dropdown-menu .form-range{ width: 100%; margin: 0; }
max-width: var(--grid-maxw, 100%);
/* =========================================================
EDITOR MODE (needs container queries)
========================================================= */
.grid-widget[data-mode="editor"]{
container-type: inline-size; /* ONLY here */
min-width: 375px;
height: 100%;
display: flex;
flex-direction: column;
}
.grid-widget[data-mode="editor"] [data-grid-wrap]{
flex: 1 1 auto;
width: 100%; width: 100%;
min-height: 375px;
position: relative;
overflow: hidden;
} }
.grid-widget[data-mode="editor"] [data-grid]{
position: absolute;
inset: 0;
width: 100%;
height: 100%;
cursor: crosshair;
touch-action: none;
z-index: 0;
}
/* Editor: toolbar should match snapped grid width */
.grid-widget[data-mode="editor"] [data-toolbar]{
width: var(--grid-maxw, 100%);
margin-inline: auto; /* center it to match the centered grid */
max-width: 100%;
align-self: center; /* don't stretch full parent width */
}
/* =========================================================
VIEWER MODE (must shrink-wrap like an <img>)
========================================================= */
.grid-widget[data-mode="viewer"]{
/* explicitly undo any containment */
container-type: normal; /* <-- the money line */
contain: none;
display: inline-block;
vertical-align: middle;
width: auto;
height: auto;
min-width: 0;
flex: none;
}
/* wrap is the sized box (JS sets px) */
.grid-widget[data-mode="viewer"] [data-grid-wrap]{
display: inline-block;
position: relative;
overflow: hidden;
line-height: 0; /* remove inline baseline gap */
}
/* grid must be in-flow and fill wrap */
.grid-widget[data-mode="viewer"] [data-grid]{
display: block;
width: 100%;
height: 100%;
cursor: default;
overflow: hidden;
}
/* viewer hides editor-only overlays */
.grid-widget[data-mode="viewer"] [data-coords],
.grid-widget[data-mode="viewer"] [data-dot]{ display: none !important; }

View file

@ -1,14 +1,25 @@
(function bindGridGlobalKeydownOnce() { (function bindGridGlobalOnce() {
if (window.__gridKeydownBound) return; if (window.__gridGlobalBound) return;
window.__gridKeydownBound = true; window.__gridGlobalBound = true;
window.activeGridWidget = null; window.activeGridWidget = null;
// Keydown (undo/redo, escape)
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
const w = window.activeGridWidget; const w = window.activeGridWidget;
if (!w) return; if (!w || typeof w.handleKeyDown !== 'function') return;
w.handleKeyDown(e); w.handleKeyDown(e);
}); });
// Pointer finalize (for drawing finishing outside the element)
const forwardPointer = (e) => {
const w = window.activeGridWidget;
if (!w || typeof w.handleGlobalPointerUp !== 'function') return;
w.handleGlobalPointerUp(e);
};
window.addEventListener('pointerup', forwardPointer, { capture: true });
window.addEventListener('pointercancel', forwardPointer, { capture: true });
})(); })();
function initGridWidget(root, opts = {}) { function initGridWidget(root, opts = {}) {
@ -190,6 +201,12 @@ function initGridWidget(root, opts = {}) {
ctx = canvasEl.getContext('2d'); ctx = canvasEl.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.save();
ctx.globalAlpha = 1;
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 10, 10);
ctx.restore();
redrawAll(); redrawAll();
} }
@ -280,6 +297,7 @@ function initGridWidget(root, opts = {}) {
if (!ctx || !shapes) return; if (!ctx || !shapes) return;
clearCanvas(); clearCanvas();
ctx.save(); ctx.save();
if (mode !== 'editor') { if (mode !== 'editor') {
ctx.translate(viewerOffset.x, viewerOffset.y); ctx.translate(viewerOffset.x, viewerOffset.y);
@ -308,13 +326,13 @@ function initGridWidget(root, opts = {}) {
const hCells = b ? (b.maxY - b.minY + padCells * 2) : 10; const hCells = b ? (b.maxY - b.minY + padCells * 2) : 10;
const wPx = Math.max(1, Math.ceil(wCells * cellSize)); const wPx = Math.max(1, Math.ceil(wCells * cellSize));
const wPy = Math.max(1, Math.ceil(hCells * cellSize)); const hPx = Math.max(1, Math.ceil(hCells * cellSize));
gridEl.style.width = `${wPx}px`; gridEl.style.width = `${wPx}px`;
gridEl.style.height = `${wPy}px`; gridEl.style.height = `${hPx}px`;
gridWrapEl.style.width = `${wPx}px`; gridWrapEl.style.width = `${wPx}px`;
gridWrapEl.style.height = `${wPy}px`; gridWrapEl.style.height = `${hPx}px`;
if (b) { if (b) {
viewerOffset.x = (-b.minX + padCells) * cellSize; viewerOffset.x = (-b.minX + padCells) * cellSize;
@ -465,6 +483,11 @@ function initGridWidget(root, opts = {}) {
currentShape = null; currentShape = null;
redrawAll(); redrawAll();
} }
},
handleGlobalPointerUp(e) {
// Only finalize if this widget is the active one (it should be)
finishPointer(e);
} }
}; };
@ -565,6 +588,7 @@ function initGridWidget(root, opts = {}) {
} }
function finishPointer(e) { function finishPointer(e) {
if (window.activeGridWidget !== api) return;
if (!currentShape) return; if (!currentShape) return;
if (activePointerId !== null && e.pointerId !== activePointerId) return; if (activePointerId !== null && e.pointerId !== activePointerId) return;
@ -1004,8 +1028,6 @@ function initGridWidget(root, opts = {}) {
} }
gridEl.addEventListener('pointerup', finishPointer); gridEl.addEventListener('pointerup', finishPointer);
window.addEventListener('pointerup', finishPointer);
window.addEventListener('pointercancel', finishPointer);
root.querySelectorAll('input[data-tool]').forEach((input) => { root.querySelectorAll('input[data-tool]').forEach((input) => {
input.addEventListener('change', () => { input.addEventListener('change', () => {
@ -1289,7 +1311,12 @@ function initGridWidget(root, opts = {}) {
if (mode !== 'editor') { if (mode !== 'editor') {
const script = root.querySelector('[data-grid-doc]'); const script = root.querySelector('[data-grid-doc]');
if (script?.textContent?.trim()) { if (script?.textContent?.trim()) {
try { api.setDoc(JSON.parse(script.textContent)); } catch { } try {
const parsed = JSON.parse(script.textContent);
api.setDoc(parsed);
} catch (err) {
console.error("viewer JSON.parse failed:", err, script.textContent);
}
return; return;
} }

View file

@ -244,22 +244,23 @@
{% endmacro %} {% endmacro %}
{% macro viewWidget(uid, json) %} {% macro viewWidget(uid, json) %}
<div class="grid-widget" data-grid-widget data-mode="viewer" data-storage-key="gridDoc:{{ uid }}"> <span class="grid-widget" data-grid-widget data-mode="viewer" data-storage-key="gridDoc:{{ uid }}">
<div class="grid-wrap" data-grid-wrap> <span class="grid-wrap" data-grid-wrap>
<span class="position-absolute p-0 m-0 d-none dot" data-dot> <span class="position-absolute p-0 m-0 d-none dot" data-dot>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" data-dot-svg> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" data-dot-svg>
<circle cx="16" cy="16" r="4" fill="black" /> <circle cx="16" cy="16" r="4" fill="black" />
</svg> </svg>
</span> </span>
<div class="position-relative overflow-hidden grid" data-grid> <span class="position-relative overflow-hidden grid" data-grid>
<div class="border border-black position-absolute d-none bg-warning-subtle px-1 py-0 user-select-none coords" <span
data-coords></div> class="border border-black position-absolute d-none bg-warning-subtle px-1 py-0 user-select-none coords"
<canvas class="position-absolute w-100 h-100" data-canvas></canvas> data-coords></span>
</div> <canvas class="position-absolute" data-canvas></canvas>
</div> </span>
</span>
<script type="application/json" data-grid-doc> <script type="application/json" data-grid-doc>
{{ json | safe }} {{ json | safe }}
</script> </script>
</div> </span>
{% endmacro %} {% endmacro %}

View file

@ -143,16 +143,10 @@
] ]
} }
{% endset %} {% endset %}
<div class="row">
{#
<div class="col" style="min-height: 80vh">
{{ draw.drawWidget('test1') }} {{ draw.drawWidget('test1') }}
</div> I am testing a thing.
#}
<div class="col" style="min-height: 80vh">
{{ draw.viewWidget('test2', jsonImage) }} {{ draw.viewWidget('test2', jsonImage) }}
</div> The thing has been tested.
</div>
{% endblock %} {% endblock %}
{% block scriptincludes %} {% block scriptincludes %}