diff --git a/inventory/static/css/components/draw.css b/inventory/static/css/components/draw.css
index d221157..df65191 100644
--- a/inventory/static/css/components/draw.css
+++ b/inventory/static/css/components/draw.css
@@ -1,258 +1,224 @@
-:root {
- --tb-h: 34px;
-}
+:root { --tb-h: 34px; }
-.grid-widget {
- container-type: inline-size;
- min-width: 375px;
- height: 100%;
- display: flex;
- flex-direction: column;
-}
+/* =========================================================
+ GRID WIDGET (editor uses container queries, viewer does not)
+ ========================================================= */
-.grid-widget .grid-wrap {
- flex: 1 1 auto;
- width: 100%;
+/* -------------------------
+ Shared basics (both modes)
+ ------------------------- */
+
+.grid-widget { /* no container-type here */ }
+
+/* drawing stack */
+.grid-widget [data-grid] {
position: relative;
- overflow: hidden;
- min-height: 375px;
- z-index: 0;
+ margin-inline: auto;
}
-.grid-widget [data-toolbar].toolbar {
- display: grid !important;
- grid-template-rows: auto auto;
- align-content: start;
- gap: .5rem;
- overflow: visible;
+/* Overlay elements */
+.grid-widget [data-canvas],
+.grid-widget [data-dot],
+.grid-widget [data-coords] { position: absolute; }
+
+.grid-widget [data-canvas]{
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ display: block;
+ z-index: 1;
+ pointer-events: none;
}
-.grid-widget [data-toolbar] .toolbar-row {
- display: flex;
- align-items: center;
- gap: .5rem;
- min-width: 0;
- flex-wrap: nowrap;
+.grid-widget [data-dot]{
+ transform: translate(-50%, -50%);
+ z-index: 2;
+ pointer-events: none;
+}
+
+.grid-widget [data-coords]{
+ bottom: 10px;
+ left: 10px;
+ pointer-events: none;
+}
+
+/* -------------------------
+ Toolbar styling
+ ------------------------- */
+
+.grid-widget [data-toolbar].toolbar{
+ display: grid !important;
+ grid-template-rows: auto auto;
+ align-content: start;
+ gap: 0.5rem;
+ overflow: visible;
+}
+
+.grid-widget [data-toolbar] .toolbar-row{
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ min-width: 0;
+ flex-wrap: nowrap;
}
.grid-widget [data-toolbar] .toolbar-row--primary,
-.grid-widget [data-toolbar] .toolbar-row--secondary {
+.grid-widget [data-toolbar] .toolbar-row--secondary{
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
+.grid-widget [data-toolbar] .toolbar-row--secondary{ opacity: 0.95; }
+
+/* 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] .toolbar-row{ display: contents; }
+
+ .grid-widget [data-toolbar] .toolbar-row--primary,
+ .grid-widget [data-toolbar] .toolbar-row--secondary{ overflow: visible; }
}
-.grid-widget [data-toolbar] .toolbar-row--secondary {
- opacity: .95;
-}
+.grid-widget [data-toolbar]::-webkit-scrollbar{ height: 8px; }
-.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;
- }
-}
-
-.grid-widget [data-grid] {
- position: absolute;
- cursor: crosshair;
- width: 100%;
- height: 100%;
- touch-action: none;
- margin: 0 auto;
- max-width: 100%;
- z-index: 0;
- inset: 0;
-}
-
-.grid-widget [data-canvas],
-.grid-widget [data-coords],
-.grid-widget [data-dot] {
- 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] {
- z-index: 1;
- pointer-events: none;
- inset: 0;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- display: block;
-}
-
-.grid-widget [data-dot] {
- transform: translate(-50%, -50%);
- z-index: 2;
- pointer-events: none;
-}
-
-.grid-widget .toolbar-range {
- width: 120px;
- margin: 0;
- flex: 0 0 120px;
-}
-
-.grid-widget .dropdown-menu {
- min-width: 200px;
- padding: .5rem .75rem;
- border-radius: .75rem;
- box-shadow: 0 10px 30px rgba(0, 0, 0, .12);
- position: absolute;
- z-index: 1000 !important;
- pointer-events: auto;
-}
-
-.grid-widget .dropdown-menu>*:first-child {
- margin-top: 0;
-}
-
-.grid-widget .dropdown-menu>*:last-child {
- margin-bottom: 0;
-}
-
-.grid-widget .dropdown-menu .form-range {
- width: 100%;
- margin: 0;
-}
-
-.grid-widget [data-toolbar] .badge,
-.grid-widget [data-toolbar] .input-group-text {
- white-space: nowrap;
-}
-
-.grid-widget [data-toolbar]>* {
- 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 {
- display: flex;
- align-items: center;
- gap: .25rem;
- padding: .25rem;
- border: 1px solid rgba(0, 0, 0, .08);
- border-radius: .5rem;
- background: rgba(0, 0, 0, .02);
+.grid-widget .toolbar-group{
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ padding: 0.25rem;
+ border: 1px solid rgba(0,0,0,0.08);
+ border-radius: 0.5rem;
+ background: rgba(0,0,0,0.02);
}
.grid-widget .btn,
.grid-widget .form-control,
-.grid-widget .badge {
- height: var(--tb-h);
+.grid-widget .badge{ height: var(--tb-h); }
+
+.grid-widget [data-toolbar] .badge,
+.grid-widget [data-toolbar] .input-group-text{ white-space: nowrap; }
+
+.grid-widget .toolbar .btn{
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 0.5rem;
}
-.grid-widget .toolbar .btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 0 .5rem;
+.grid-widget .toolbar .form-control-color{ width: var(--tb-h); padding: 0; }
+
+.grid-widget .tb-btn{ flex-direction: column; gap: 2px; line-height: 1; }
+.grid-widget .tb-btn small{ font-size: 11px; opacity: 0.75; }
+
+.grid-widget .dropdown-toggle::after{ display: none; }
+
+.grid-widget .toolbar .btn-group .btn{ border-radius: 0; }
+.grid-widget .toolbar .btn-group .btn:first-child{
+ border-top-left-radius: 0.5rem;
+ border-bottom-left-radius: 0.5rem;
+}
+.grid-widget .toolbar .btn-group .btn:last-child{
+ border-top-right-radius: 0.5rem;
+ border-bottom-right-radius: 0.5rem;
}
-.grid-widget .toolbar .form-control-color {
- width: var(--tb-h);
- padding: 0;
+.grid-widget .btn-check:checked + .btn{
+ background: rgba(0,0,0,0.08);
+ border-color: rgba(0,0,0,0.18);
}
-.grid-widget .tb-btn {
- flex-direction: column;
- gap: 2px;
- line-height: 1;
+.grid-widget .dropdown-menu{
+ min-width: 200px;
+ 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 .tb-btn small {
- font-size: 11px;
- opacity: .75;
+.grid-widget .dropdown-menu .form-range{ width: 100%; margin: 0; }
+
+/* =========================================================
+ 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 .dropdown-toggle::after {
- display: none;
+.grid-widget[data-mode="editor"] [data-grid-wrap]{
+ flex: 1 1 auto;
+ width: 100%;
+ min-height: 375px;
+ position: relative;
+ overflow: hidden;
}
-.grid-widget .toolbar .btn-group .btn {
- border-radius: 0;
+.grid-widget[data-mode="editor"] [data-grid]{
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ cursor: crosshair;
+ touch-action: none;
+ z-index: 0;
}
-.grid-widget .toolbar .btn-group .btn:first-child {
- border-top-left-radius: .5rem;
- border-bottom-left-radius: .5rem;
+/* 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 */
}
-.grid-widget .toolbar .btn-group .btn:last-child {
- border-top-right-radius: .5rem;
- border-bottom-right-radius: .5rem;
+/* =========================================================
+ VIEWER MODE (must shrink-wrap like an )
+ ========================================================= */
+
+.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;
}
-.grid-widget .btn-check:checked+.btn {
- background: rgba(0, 0, 0, .08);
- border-color: rgba(0, 0, 0, .18);
+/* 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-widget [data-toolbar],
-.grid-widget [data-grid] {
- margin-inline: auto;
+/* 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;
}
-.grid-widget [data-toolbar] {
- max-width: var(--grid-maxw, 100%);
- width: 100%;
-}
\ No newline at end of file
+/* viewer hides editor-only overlays */
+.grid-widget[data-mode="viewer"] [data-coords],
+.grid-widget[data-mode="viewer"] [data-dot]{ display: none !important; }
diff --git a/inventory/static/js/components/draw.js b/inventory/static/js/components/draw.js
index 03546e1..4f941f7 100644
--- a/inventory/static/js/components/draw.js
+++ b/inventory/static/js/components/draw.js
@@ -1,14 +1,25 @@
-(function bindGridGlobalKeydownOnce() {
- if (window.__gridKeydownBound) return;
- window.__gridKeydownBound = true;
+(function bindGridGlobalOnce() {
+ if (window.__gridGlobalBound) return;
+ window.__gridGlobalBound = true;
window.activeGridWidget = null;
+ // Keydown (undo/redo, escape)
document.addEventListener('keydown', (e) => {
const w = window.activeGridWidget;
- if (!w) return;
+ if (!w || typeof w.handleKeyDown !== 'function') return;
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 = {}) {
@@ -190,6 +201,12 @@ function initGridWidget(root, opts = {}) {
ctx = canvasEl.getContext('2d');
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();
}
@@ -280,6 +297,7 @@ function initGridWidget(root, opts = {}) {
if (!ctx || !shapes) return;
clearCanvas();
+
ctx.save();
if (mode !== 'editor') {
ctx.translate(viewerOffset.x, viewerOffset.y);
@@ -308,15 +326,15 @@ function initGridWidget(root, opts = {}) {
const hCells = b ? (b.maxY - b.minY + padCells * 2) : 10;
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.height = `${wPy}px`;
+ gridEl.style.height = `${hPx}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.y = (-b.minY + padCells) * cellSize;
} else {
@@ -465,6 +483,11 @@ function initGridWidget(root, opts = {}) {
currentShape = null;
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) {
+ if (window.activeGridWidget !== api) return;
if (!currentShape) return;
if (activePointerId !== null && e.pointerId !== activePointerId) return;
@@ -1004,8 +1028,6 @@ function initGridWidget(root, opts = {}) {
}
gridEl.addEventListener('pointerup', finishPointer);
- window.addEventListener('pointerup', finishPointer);
- window.addEventListener('pointercancel', finishPointer);
root.querySelectorAll('input[data-tool]').forEach((input) => {
input.addEventListener('change', () => {
@@ -1289,7 +1311,12 @@ function initGridWidget(root, opts = {}) {
if (mode !== 'editor') {
const script = root.querySelector('[data-grid-doc]');
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;
}
diff --git a/inventory/templates/components/draw.html b/inventory/templates/components/draw.html
index bdce7d8..f6b8f19 100644
--- a/inventory/templates/components/draw.html
+++ b/inventory/templates/components/draw.html
@@ -244,22 +244,23 @@
{% endmacro %}
{% macro viewWidget(uid, json) %}
-