diff --git a/.editorconfig b/.editorconfig index e100c8b358941f02aed25a61b9a3344bbcc86363..0f356b69da2004822321df807706a555dffd5bad 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,7 @@ end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true -[*.{json,less,ts,html.j2}] +[*.{json,less,ts,html.j2,css}] indent_style = tab indent_size = 4 diff --git a/examples/static/schild.css b/examples/static/schild.css index 1616d564c1d565154eff6c56893106a59532c644..44cf298f895abb3ea7dcc3c95bc7fa66aaafd40b 100644 --- a/examples/static/schild.css +++ b/examples/static/schild.css @@ -1,83 +1,86 @@ @page { - size: a4 landscape; - margin: 50px; + size: a4 landscape; + margin: 50px; } * { - box-sizing: border-box; + box-sizing: border-box; } body { - font-size: 48px; - font-family: "Noto Sans", sans-serif; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - @media not print { - /* This magic constant approximately corresponds to 48px at A4 size, but scales - * with the viewport/iframe size - */ - font-size: 4.27426536064114vw; - padding: 4.27426536064114vw; - width: 100vw; - height: 100vh; - max-width: 100vw; - max-height: 100vh; - } + font-size: 48px; + font-family: "Noto Sans", sans-serif; + padding: 0; + margin: 0; + background-color: #fff; + color: #000; + display: flex; + flex-direction: column; + + @media not print { + /* This magic constant approximately corresponds to 48px at A4 size, but scales + * with the viewport/iframe size + */ + font-size: 4.27426536064114vw; + padding: 4.27426536064114vw; + width: 100vw; + height: 100vh; + max-width: 100vw; + max-height: 100vh; + } } main { - flex-grow: 1; - max-height: 11.3em; + flex-grow: 1; + max-height: 11.3em; } img { - max-width: 100%; - max-height: 100%; - object-fit: contain; - display: block; + max-width: 100%; + max-height: 100%; + object-fit: contain; + display: block; } #logo { - width: 15%; + width: 15%; } footer { - font-size: 0.5em; - display: flex; - align-items: flex-end; + font-size: 0.5em; + display: flex; + align-items: flex-end; } #footer { - flex-grow: 1; + flex-grow: 1; } h1 { - margin-top: 0; + margin-top: 0; } p { - margin-top: 0; - margin-bottom: 0; + margin-top: 0; + margin-bottom: 0; } .container { - column-count: 2; - column-fill: balance; + column-count: 2; + column-fill: balance; } .container > img, .container > svg { - max-width: 100%; - max-height: 8em; - display: block; - margin-left: auto; - margin-right: auto; + max-width: 100%; + max-height: 8em; + display: block; + margin-left: auto; + margin-right: auto; } .box, svg { - width: 100%; - margin: 0; - padding: 0; + width: 100%; + margin: 0; + padding: 0; } diff --git a/frontend/src/main.less b/frontend/src/main.less index 89c0e5389c3cf04c199ab00d8964d84ca7b59c06..966bf21fdf405d3ba15668e8397d989ad43d8a92 100644 --- a/frontend/src/main.less +++ b/frontend/src/main.less @@ -1,5 +1,12 @@ // the threshold at which we consider the screen "mobile" @mobile_threshold: 700px; +// the threshold at which we consider the screen "wide", i.e. display the preview on the +// side instead of at the bottom +@wide_threshold: 1700px; +// the border around the options in the fieldset-select-like things +@option_border: .3rem; +// the minimum size of the left area in the form layout +@label-width: 90px; * { box-sizing: border-box; @@ -13,6 +20,7 @@ body { // adjust colors for light theme here --bg: #fff; --fg: #000; + --fg-dark: #bbb; --accent: #80f; --accent2: #c4f; @@ -20,10 +28,39 @@ body { // adjust colors for dark theme here --bg: #222; --fg: #fff; + --fg-dark: #888; } +} + +@inputs: ~'input[type = "text"], input[type = "number"], textarea, select'; +@buttons: ~'input[type = "submit"], button, a.button'; + +body, @{inputs}, @{buttons} { + &, &:hover, &:visited { + background-color: var(--bg); + color: var(--fg); + // ja, user agents haben lack gesoffen + font-size: 1rem; + } +} - background-color: var(--bg); - color: var(--fg); +@{inputs}, @{buttons}, fieldset { + border: .15rem solid var(--fg-dark); + border-radius: .25rem; +} + +@{inputs} { + padding: .2rem .3rem; +} + +@{buttons} { + padding: .2rem .6rem; + + &:hover { + border-color: var(--accent2); + cursor: pointer; + text-decoration: none; + } } a { @@ -127,84 +164,126 @@ main { } } +.section-container { + display: grid; + grid-gap: 1rem; + grid-template-columns: 1fr; + + .section { + grid-column-start: 1; + } + + @media screen and (min-width: @wide_threshold) { + grid-template-columns: 1fr 1fr; + + .preview-section { + grid-column-start: 2; + grid-row: 1/span 3; + } + } +} + .preview { - width: min(95vw, 100rem); /* A4 aspect ratio: √2:1 */ aspect-ratio: 1.4142135623730951; - - background-color: #fff; - color: #000; } -.preview-small { - width: 15rem; +.preview-container .preview { + width: 100%; } -.preview-label { - place-self: center; -} +// this is the form layout row container thingy +.box { + display: grid; + grid-template-columns: min(@label-width, auto) 1fr; + align-items: baseline; -form { - display: inline-block; -} + label.for-text { + display: inline-block; + justify-self: end; + font-size: 1.1rem; + } -@label-padding: 90px; + input[type = "text"], textarea { + justify-self: stretch; + } -label.for-text { - display: inline-block; - min-width: @label-padding; - text-align: right; + textarea { + height: 5rem; + } } -input[type="text"], textarea { - width: 300px; +// this is the form layout row with buttons in it +.buttons { + display: flex; + justify-content: flex-end; + align-items: stretch; } -textarea { - height: 5em; - vertical-align: top; +.box, .buttons { + margin: .5rem 0; + gap: 1ch; } -fieldset { +// these are the fieldsets containing select-like options modeled with radio buttons +.select { display: flex; flex-wrap: wrap; -} + gap: 2 * @option_border; -fieldset > div { - display: flex; - gap: 0.5rem; - flex-direction: column; -} + .option { + display: flex; + flex-direction: column; + gap: .5rem; -.imageselect > div { - width: 10rem; - display: flex; - gap: 0.5rem; -} + .preview { + width: 15rem; + cursor: pointer; + } -fieldset label { - flex-grow: 1; -} + .preview-label { + place-self: center; + } -.imageselect img { - width: 100%; - background-color: #fff; -} + img { + width: 10rem; + background-color: #fff; + } -fieldset input { - display: none; -} + .option-border { + border: @option_border solid var(--fg-dark); + border-radius: 3 * @option_border; + + // iframe's behave nicely + &.preview { + padding: @option_border; + } + + // label>img doesn't + & > img { + border: @option_border solid var(--bg); + border-radius: 2 * @option_border; + object-fit: contain; + } + } -fieldset img, -fieldset iframe, -.preview-small { - border: 3px solid lightgray; - border-radius: 10px; -} + input[type = "radio"] { + display: none; -input[type="radio"]:checked { - & + label > img, - & + iframe { - border:3px solid red; + &:checked + .option-border { + border-color: var(--accent2); + } + } + + label { + flex-grow: 1; + // firefox hat hier lack gesoffen und erfindet 5px wenn das hier block oder + // inline ist + display: flex; + } + } + + input[type = "radio"] { + display: none; } } diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 23650e5fde1a929fa2a497f0a619fdb4e486688f..6a987728de755d1826356c93bb8e57640c1028c7 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,7 +1,5 @@ import './main.less'; -console.log("Hello World"); - function onInputHandler(event: Event) { const preview = document.getElementById('preview') as HTMLIFrameElement; const previewDoc = preview.contentDocument as Document; @@ -12,9 +10,30 @@ function onInputHandler(event: Event) { window.addEventListener("load", () => { if (document.getElementById('preview')) { - for (let el of document.getElementsByClassName('input-dispatch')) { + for (const el of document.getElementsByClassName('input-dispatch')) { console.log(el); el.addEventListener('input', onInputHandler); } + + for (const op of document.querySelectorAll('.option .preview')) { + console.log(op); + const input_id = (op as HTMLElement).dataset.for; + if (!input_id) { + console.error("Missing data-for attribute for", op); + continue; + } + const input = document.getElementById(input_id); + if (!input || input === null) { + console.error("Unable to find input for", op); + continue; + } + + const handler = (_: Event) => { + console.log("clicked"); + (input as HTMLInputElement).checked = true; + }; + op.addEventListener('click', handler); + (op as HTMLIFrameElement).contentDocument?.addEventListener('click', handler); + } } }); diff --git a/schilder2000/templates/schild.html.j2 b/schilder2000/templates/schild.html.j2 index f4136d6baa39199ec85ca26b476e4c4bf6a5be9b..615a8781823de4c567601591b6f50f8a4939d252 100644 --- a/schilder2000/templates/schild.html.j2 +++ b/schilder2000/templates/schild.html.j2 @@ -28,111 +28,114 @@ {%- endblock title %} {% block main -%} - <section> - {%- with messages = get_flashed_messages() -%} - {%- if messages -%} - <ul class="flashes"> - {%- for m in messages -%} - <li>{{ m }}</li> - {%- endfor -%} - </ul> - {%- endif -%} - {%- endwith -%} - </section> + <div class="section-container"> + <div class="section"> + {%- with messages = get_flashed_messages() -%} + {%- if messages -%} + <ul class="flashes"> + {%- for m in messages -%} + <li>{{ m }}</li> + {%- endfor -%} + </ul> + {%- endif -%} + {%- endwith -%} + </div> - <section> - <form method="post" action=""> - {{ form.csrf_token }} + <div class="section"> + <form method="post" action=""> + {{ form.csrf_token }} - {{ render_field(form.title, "input-dispatch") }} - {{ render_field(form.text, "input-dispatch") }} + {{ render_field(form.title, "input-dispatch") }} + {{ render_field(form.text, "input-dispatch") }} - <fieldset class="templateselect"> - <legend>Vorlage</legend> - {%- for t in form.template.choices -%} - <div> - <input - type="radio" - name="template" - id="template:{{ t.name }}" - value="{{ t.name }}" - {% if t.name == (schild | default(None)).template -%} - checked - {%- endif -%} - /> - <iframe - class="preview preview-small" - src="{{ url_for('instance.sample_html', template=t.name) }}" - id="template-preview:{{ t.name }}" - > - </iframe> - <label for="template:{{ t.name }}" class="preview-label"> - {{ t.description or t.name }} - </label> - </div> - {% endfor %} - </fieldset> + <fieldset class="select templateselect"> + <legend>Vorlage</legend> + {%- for t in form.template.choices -%} + <div class="option"> + <input + type="radio" + name="template" + id="template:{{ t.name }}" + value="{{ t.name }}" + {% if t.name == (schild | default(None)).template -%} + checked + {%- endif -%} + /> + <iframe + class="preview option-border" + src="{{ url_for('instance.sample_html', template=t.name) }}" + id="template-preview:{{ t.name }}" + data-for="template:{{ t.name }}" + > + </iframe> + <label for="template:{{ t.name }}" class="preview-label"> + {{ t.description or t.name }} + </label> + </div> + {% endfor %} + </fieldset> - <fieldset class="imageselect"> - <legend>Bild</legend> - {%- for img in form.image.choices -%} - <div> - <input - type="radio" - name="image" - id="img:{{ img }}" - value="{{ img }}" - {% if img == (schild | default(None)).image -%} - checked - {%- endif -%} - /> - <label for="img:{{ img }}"> - <img src="{{ url_for('instance.static', filename='img/'+img) }}" title="{{ img }}" /> - </label> - </div> - {%- endfor -%} - </fieldset> - - <div class="box"> - {%- if schild -%} - <input type="submit" value="Speichern" /> - <input - type="submit" - formaction="{{ url_for('views.create') }}" - value="Neues Schild erstellen" - /> - {%- else -%} - <input type="submit" value="Schild erstellen" /> - {%- endif -%} - </div> - </form> - </section> + <fieldset class="select imageselect"> + <legend>Bild</legend> + {%- for img in form.image.choices -%} + <div class="option"> + <input + type="radio" + name="image" + id="img:{{ img }}" + value="{{ img }}" + {% if img == (schild | default(None)).image -%} + checked + {%- endif -%} + /> + <label for="img:{{ img }}" class="option-border"> + <img src="{{ url_for('instance.static', filename='img/'+img) }}" title="{{ img }}" /> + </label> + </div> + {%- endfor -%} + </fieldset> - {%- if schild %} - <div> - <a href="{{ url_for('instance.schild_pdf', ident=schild.ident) }}"> - Als PDF anzeigen - </a> + <div class="buttons"> + {%- if schild -%} + <input type="submit" value="Speichern" /> + <input + type="submit" + formaction="{{ url_for('views.create') }}" + value="Neues Schild erstellen" + /> + {%- else -%} + <input type="submit" value="Schild erstellen" /> + {%- endif -%} + </div> + </form> </div> - <section><form method="post" action="{{ url_for('.print', ident=schild.ident) }}"> - {%- for field in printform -%} - {{ render_field(field) }} - {%- endfor -%} - - <input type="submit" value="Drucken" /> - </form></section> + {%- if schild %} + <div class="section"> + <form method="post" action="{{ url_for('.print', ident=schild.ident) }}"> + {%- for field in printform -%} + {{ render_field(field) }} + {%- endfor -%} + <div class="buttons"> + <a class="button" href="{{ url_for('instance.schild_pdf', ident=schild.ident) }}"> + Als PDF anzeigen + </a> + <input type="submit" value="Drucken" /> + </div> + </form> + </div> - <div> - <h2>Vorschau</h2> - <div class="preview-container" id="preview-container"> - <iframe - class="preview" - id="preview" - src="{{ url_for('instance.schild_html', ident=schild.ident) }}"> - </iframe> + <div class="section preview-section"> + <h2>Vorschau</h2> + <div class="preview-container" id="preview-container"> + <iframe + class="preview" + id="preview" + src="{{ url_for('instance.schild_html', ident=schild.ident) }}"> + </iframe> + </div> </div> - </div> - {% endif %} + {% endif %} + </div> {% endblock main %}