05/04 Update form creation page
This commit is contained in:
@@ -36,28 +36,45 @@
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
/* ── iPad / touch-device fluid grid override ── */
|
||||
/* ── iPad / touch-device fluid grid override ──
|
||||
--fg-w is set by ResizeObserver in JS (see bottom of file),
|
||||
which measures the actual .insp-body clientWidth — no vw/scrollbar ambiguity.
|
||||
Falls back to a safe 100% - padding calculation if JS hasn't run yet. */
|
||||
@media (max-width: 1194px) {
|
||||
.form-grid {
|
||||
/* Tighter column gap (4px) so empty intermediate columns between fields
|
||||
don't create a large visual gap — e.g. between rating stars and upload */
|
||||
--_gap: 4px;
|
||||
--_cell: calc((min(calc(100vw - 2rem), 900px) - 11 * 4px) / 12);
|
||||
grid-template-columns: repeat(12, var(--_cell));
|
||||
grid-auto-rows: minmax(calc(var(--_cell) * 0.72), auto);
|
||||
gap: 4px var(--_gap);
|
||||
--fg-gap: 4px;
|
||||
--fg-w: calc(100% - 2rem); /* JS overrides this with measured px value */
|
||||
--fg-cell: calc((var(--fg-w) - 11 * var(--fg-gap)) / 12);
|
||||
grid-template-columns: repeat(12, var(--fg-cell));
|
||||
grid-auto-rows: minmax(calc(var(--fg-cell) * 0.72), auto);
|
||||
gap: var(--fg-gap);
|
||||
width: 100%;
|
||||
}
|
||||
.insp-body { padding: 1rem; }
|
||||
|
||||
/* Rating stars: scale down to fit compressed grid cells on iPad */
|
||||
.rating-stars button { font-size: .85rem; }
|
||||
.rating-stars button { font-size: .9rem; min-height: 36px; min-width: 28px; }
|
||||
|
||||
/* Pass/fail pills: allow wrapping on narrow cells */
|
||||
.pf-group { flex-wrap: wrap !important; gap: .4rem !important; }
|
||||
.pf-btn { flex-shrink: 0; }
|
||||
|
||||
/* Image upload cells: don't clip the chosen-file row */
|
||||
.upload-zone { overflow: visible; }
|
||||
|
||||
/* Touch: larger tap targets for checkboxes and radios */
|
||||
.form-check-input { width: 1.1em; height: 1.1em; margin-top: .15rem; }
|
||||
}
|
||||
.fg-cell {
|
||||
overflow:hidden; display:flex; flex-direction:column; padding:.18rem .45rem;
|
||||
/* overflow:visible so content (dropdowns, tall labels) is never silently clipped.
|
||||
align-content:start keeps items pinned to the top of the cell. */
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: start;
|
||||
padding: .18rem .45rem;
|
||||
/* Min height ensures touch targets are reachable even on very short row spans */
|
||||
min-height: 36px;
|
||||
}
|
||||
.fg-cell .field-lbl {
|
||||
font-size:.74rem; font-weight:500; color:#64748b;
|
||||
@@ -119,7 +136,27 @@
|
||||
}
|
||||
.upload-clear:hover { background:#fecaca; }
|
||||
|
||||
.signature-pad-wrap {
|
||||
/* ── Pass / Fail pills ── */
|
||||
.pf-group { display:flex; gap:.5rem; flex-wrap:wrap; }
|
||||
.pf-btn {
|
||||
display: flex; align-items: center; gap: .35rem;
|
||||
cursor: pointer;
|
||||
padding: .4rem .85rem; border-radius: 20px;
|
||||
font-size: .84rem; font-weight: 600;
|
||||
border: 2px solid transparent;
|
||||
transition: background .15s, color .15s, border-color .15s;
|
||||
user-select: none;
|
||||
min-height: 36px; /* 44px touch target via padding — no layout shift */
|
||||
}
|
||||
.pf-btn input[type=radio] { display: none; }
|
||||
/* Pass variants */
|
||||
.pf-btn.pf-pass { border-color: #16a34a; color: #16a34a; background: transparent; }
|
||||
.pf-btn.pf-pass.pf-on,
|
||||
.pf-btn.pf-pass:has(input:checked) { background: #16a34a; color: #fff; }
|
||||
/* Fail variants */
|
||||
.pf-btn.pf-fail { border-color: #dc2626; color: #dc2626; background: transparent; }
|
||||
.pf-btn.pf-fail.pf-on,
|
||||
.pf-btn.pf-fail:has(input:checked) { background: #dc2626; color: #fff; }
|
||||
flex:1; min-height:0; border-radius:6px; border:1px solid #e2e8f0;
|
||||
background:#f8fafc; position:relative; overflow:hidden;
|
||||
}
|
||||
@@ -334,20 +371,14 @@
|
||||
{% elif field.type == 'pass_fail' %}
|
||||
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
|
||||
{% set pf_options = field.options if field.options else ['Pass', 'Fail'] %}
|
||||
<div class="pf-group" style="display:flex;gap:.5rem;flex-wrap:wrap;">
|
||||
<div class="pf-group">
|
||||
{% for opt in pf_options %}
|
||||
{% set is_pass = opt.lower() in ('pass','yes','ok','good') %}
|
||||
<label class="pf-btn{{ ' active' if saved == opt }}" data-color="{{ '#16a34a' if is_pass else '#dc2626' }}"
|
||||
style="display:flex;align-items:center;gap:.35rem;cursor:pointer;
|
||||
padding:.4rem .85rem;border-radius:20px;font-size:.84rem;font-weight:600;
|
||||
border:2px solid {{ '#16a34a' if is_pass else '#dc2626' }};
|
||||
color:{{ ('#fff' if saved == opt else ('#16a34a' if is_pass else '#dc2626')) }};
|
||||
background:{{ (('#16a34a' if is_pass else '#dc2626') if saved == opt else 'transparent') }};
|
||||
transition:background .15s,color .15s;">
|
||||
{% set is_pass = opt.lower() in ('pass','yes','ok','good','acceptable','compliant') %}
|
||||
<label class="pf-btn {{ 'pf-pass' if is_pass else 'pf-fail' }}{{ ' pf-on' if saved == opt }}"
|
||||
data-fid="{{ fid }}">
|
||||
<input type="radio" name="field_{{ fid }}" value="{{ opt }}"
|
||||
{{ 'checked' if saved == opt }}
|
||||
style="display:none;"
|
||||
onchange="var g=this.closest('.pf-group');g.querySelectorAll('.pf-btn').forEach(function(l){l.style.background='';l.style.color=l.dataset.color;});this.parentElement.style.background=this.parentElement.dataset.color;this.parentElement.style.color='#fff';">
|
||||
onchange="pfSelect(this)">
|
||||
<span>{{ opt }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
@@ -474,6 +505,43 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// ── Pass / Fail pill toggle ───────────────────────────────────────────────────
|
||||
// Adds/removes .pf-on class instead of inline styles — works with CSS :has() fallback
|
||||
function pfSelect(radio) {
|
||||
const group = radio.closest('.pf-group');
|
||||
if (!group) return;
|
||||
group.querySelectorAll('.pf-btn').forEach(function(btn) {
|
||||
btn.classList.remove('pf-on');
|
||||
});
|
||||
if (radio.checked) {
|
||||
radio.closest('.pf-btn').classList.add('pf-on');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fluid grid width — ResizeObserver sets --fg-w on .form-grid ──────────────
|
||||
// Avoids 100vw (includes scrollbar) and 100% on .form-grid itself (reports 0
|
||||
// before first paint). Measures .insp-body which is always reliably sized.
|
||||
(function () {
|
||||
function applyGridWidth() {
|
||||
const body = document.querySelector('.insp-body');
|
||||
const grid = document.querySelector('.form-grid');
|
||||
if (!body || !grid) return;
|
||||
// Available width = insp-body inner width minus its horizontal padding (1rem each side on iPad)
|
||||
const style = getComputedStyle(body);
|
||||
const padH = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
||||
const available = body.clientWidth - padH;
|
||||
grid.style.setProperty('--fg-w', available + 'px');
|
||||
}
|
||||
|
||||
// Run immediately and on every resize
|
||||
applyGridWidth();
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
new ResizeObserver(applyGridWidth).observe(document.querySelector('.insp-body') || document.body);
|
||||
} else {
|
||||
window.addEventListener('resize', applyGridWidth);
|
||||
}
|
||||
}());
|
||||
|
||||
// ── Rating stars ─────────────────────────────────────────────────────────────
|
||||
function setRating(btn) {
|
||||
const group = btn.closest('.rating-stars');
|
||||
|
||||
Reference in New Issue
Block a user