nti_receipt_builder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +272 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/nti_receipt_builder/index.js +16 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_builder_controller.js +297 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_canvas_controller.js +279 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_draggable_controller.js +154 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_print_controller.js +24 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_properties_controller.js +297 -0
- data/app/assets/stylesheets/nti_receipt_builder.css +238 -0
- data/app/controllers/concerns/nti_receipt_builder/templates_controller.rb +170 -0
- data/app/helpers/nti_receipt_builder/render_helper.rb +21 -0
- data/app/models/nti_receipt_builder/template.rb +79 -0
- data/app/views/nti_receipt_builder/render/_order_lines_element.html.erb +36 -0
- data/app/views/nti_receipt_builder/render/_overflow_banner.html.erb +5 -0
- data/app/views/nti_receipt_builder/render/_paper.html.erb +12 -0
- data/app/views/nti_receipt_builder/render/_text_element.html.erb +1 -0
- data/app/views/nti_receipt_builder/render/_variable_element.html.erb +1 -0
- data/app/views/nti_receipt_builder/templates/_canvas.html.erb +33 -0
- data/app/views/nti_receipt_builder/templates/_elements_panel.html.erb +49 -0
- data/app/views/nti_receipt_builder/templates/_errors.html.erb +21 -0
- data/app/views/nti_receipt_builder/templates/_lock_version_field.html.erb +2 -0
- data/app/views/nti_receipt_builder/templates/_print_assets.html.erb +15 -0
- data/app/views/nti_receipt_builder/templates/_properties_panel.html.erb +8 -0
- data/app/views/nti_receipt_builder/templates/_settings_panel.html.erb +130 -0
- data/app/views/nti_receipt_builder/templates/create.turbo_stream.erb +3 -0
- data/app/views/nti_receipt_builder/templates/edit.html.erb +54 -0
- data/app/views/nti_receipt_builder/templates/new.html.erb +115 -0
- data/app/views/nti_receipt_builder/templates/preview.html.erb +24 -0
- data/app/views/nti_receipt_builder/templates/print.html.erb +31 -0
- data/app/views/nti_receipt_builder/templates/render_preview.turbo_stream.erb +26 -0
- data/app/views/nti_receipt_builder/templates/update.turbo_stream.erb +7 -0
- data/config/importmap.rb +4 -0
- data/lib/generators/nti_receipt_builder/install_generator.rb +49 -0
- data/lib/generators/nti_receipt_builder/templates/create_nti_receipt_templates.rb +28 -0
- data/lib/generators/nti_receipt_builder/templates/initializer.rb +16 -0
- data/lib/generators/nti_receipt_builder/templates/turbo_stream_tags.rb +36 -0
- data/lib/nti_receipt_builder/configuration.rb +59 -0
- data/lib/nti_receipt_builder/elements.rb +46 -0
- data/lib/nti_receipt_builder/engine.rb +32 -0
- data/lib/nti_receipt_builder/errors.rb +13 -0
- data/lib/nti_receipt_builder/layout_validator.rb +285 -0
- data/lib/nti_receipt_builder/presenters/collection.rb +126 -0
- data/lib/nti_receipt_builder/presenters/element.rb +68 -0
- data/lib/nti_receipt_builder/render_result.rb +18 -0
- data/lib/nti_receipt_builder/renderer.rb +78 -0
- data/lib/nti_receipt_builder/set_default.rb +24 -0
- data/lib/nti_receipt_builder/template_form.rb +60 -0
- data/lib/nti_receipt_builder/type_inference.rb +26 -0
- data/lib/nti_receipt_builder/variable_definition.rb +74 -0
- data/lib/nti_receipt_builder/variable_resolver.rb +55 -0
- data/lib/nti_receipt_builder/variables.rb +159 -0
- data/lib/nti_receipt_builder/version.rb +5 -0
- data/lib/nti_receipt_builder.rb +50 -0
- metadata +175 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
function escapeHtml(value) {
|
|
4
|
+
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
|
5
|
+
"&": "&",
|
|
6
|
+
"<": "<",
|
|
7
|
+
">": ">",
|
|
8
|
+
'"': """,
|
|
9
|
+
"'": "'"
|
|
10
|
+
}[char]))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const FONT_SIZE_MIN = 6
|
|
14
|
+
const FONT_SIZE_MAX = 72
|
|
15
|
+
const LINE_HEIGHT_MIN = 0.8
|
|
16
|
+
const LINE_HEIGHT_MAX = 3.0
|
|
17
|
+
const ROW_SPACING_MIN = 0
|
|
18
|
+
const ROW_SPACING_MAX = 10
|
|
19
|
+
const Z_INDEX_MIN = 0
|
|
20
|
+
const Z_INDEX_MAX = 1000
|
|
21
|
+
const MIN_ELEMENT_DIMENSION_MM = 1.0
|
|
22
|
+
const TEXT_MAX_LENGTH = 500
|
|
23
|
+
const FALLBACK_MAX_LENGTH = 200
|
|
24
|
+
const PREFIX_MAX_LENGTH = 50
|
|
25
|
+
const SUFFIX_MAX_LENGTH = 50
|
|
26
|
+
|
|
27
|
+
// Connects to data-controller="nrb-properties"
|
|
28
|
+
export default class extends Controller {
|
|
29
|
+
static targets = ["content"]
|
|
30
|
+
static values = { scalarVariables: Object }
|
|
31
|
+
|
|
32
|
+
connect() {
|
|
33
|
+
this.boundKeydown = this.handleKeydown.bind(this)
|
|
34
|
+
document.addEventListener("keydown", this.boundKeydown)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
disconnect() {
|
|
38
|
+
document.removeEventListener("keydown", this.boundKeydown)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
render(element) {
|
|
42
|
+
this.currentElement = element
|
|
43
|
+
|
|
44
|
+
if (!element) {
|
|
45
|
+
this.contentTarget.innerHTML =
|
|
46
|
+
'<p class="text-secondary nrb-receipt-hint">Select an element on the canvas to edit its properties.</p>'
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.contentTarget.innerHTML = this.templateFor(element)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fieldChanged(event) {
|
|
54
|
+
if (!this.currentElement) return
|
|
55
|
+
|
|
56
|
+
const field = event.params.field
|
|
57
|
+
const valueType = event.params.valueType || "string"
|
|
58
|
+
const value = this.readFieldValue(event.target, valueType)
|
|
59
|
+
const patch = this.buildPatch(field, value)
|
|
60
|
+
|
|
61
|
+
this.dispatch("changed", {
|
|
62
|
+
prefix: "receipt-properties",
|
|
63
|
+
bubbles: true,
|
|
64
|
+
detail: { id: this.currentElement.id, patch }
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
deleteElement() {
|
|
69
|
+
if (!this.currentElement) return
|
|
70
|
+
|
|
71
|
+
this.dispatch("delete-requested", {
|
|
72
|
+
prefix: "receipt-properties",
|
|
73
|
+
bubbles: true,
|
|
74
|
+
detail: { id: this.currentElement.id }
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
handleKeydown(event) {
|
|
79
|
+
if (event.key !== "Delete" && event.key !== "Backspace") return
|
|
80
|
+
if (!this.currentElement) return
|
|
81
|
+
|
|
82
|
+
const active = document.activeElement
|
|
83
|
+
const isTextEntry = active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.isContentEditable)
|
|
84
|
+
if (isTextEntry) return
|
|
85
|
+
|
|
86
|
+
event.preventDefault()
|
|
87
|
+
this.deleteElement()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
readFieldValue(target, valueType) {
|
|
91
|
+
if (valueType === "boolean") return target.checked
|
|
92
|
+
|
|
93
|
+
if (valueType === "integer") {
|
|
94
|
+
const num = parseInt(target.value, 10)
|
|
95
|
+
return Number.isNaN(num) ? 0 : num
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (valueType === "number") {
|
|
99
|
+
const num = parseFloat(target.value)
|
|
100
|
+
return Number.isNaN(num) ? 0 : num
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return target.value
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
buildPatch(field, value) {
|
|
107
|
+
const path = field.split(".")
|
|
108
|
+
if (path.length === 1) return { [path[0]]: value }
|
|
109
|
+
|
|
110
|
+
const [group, key] = path
|
|
111
|
+
return { [group]: { [key]: value } }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Template building
|
|
115
|
+
|
|
116
|
+
templateFor(element) {
|
|
117
|
+
return [this.commonFieldsTemplate(element), this.typeFieldsTemplate(element), this.deleteButtonTemplate()].join("")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
commonFieldsTemplate(element) {
|
|
121
|
+
return `
|
|
122
|
+
<div class="row g-2">
|
|
123
|
+
<div class="col-6">${this.numberField("X (mm)", "x_mm", element.x_mm, { min: 0, step: 0.1 })}</div>
|
|
124
|
+
<div class="col-6">${this.numberField("Y (mm)", "y_mm", element.y_mm, { min: 0, step: 0.1 })}</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="row g-2">
|
|
127
|
+
<div class="col-6">${this.numberField("Width (mm)", "width_mm", element.width_mm, { min: MIN_ELEMENT_DIMENSION_MM, step: 0.1 })}</div>
|
|
128
|
+
<div class="col-6">${this.numberField("Height (mm)", "height_mm", element.height_mm, { min: MIN_ELEMENT_DIMENSION_MM, step: 0.1 })}</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="row g-2">
|
|
131
|
+
<div class="col-6">${this.numberField("Layer (z-index)", "z_index", element.z_index || 0, { min: Z_INDEX_MIN, max: Z_INDEX_MAX, step: 1, valueType: "integer" })}</div>
|
|
132
|
+
<div class="col-6 d-flex align-items-end">${this.checkboxField("Visible", "visible", element.visible !== false)}</div>
|
|
133
|
+
</div>
|
|
134
|
+
`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
typeFieldsTemplate(element) {
|
|
138
|
+
if (element.type === "variable") return this.variableFieldsTemplate(element)
|
|
139
|
+
if (element.type === "text") return this.textFieldsTemplate(element)
|
|
140
|
+
return this.orderLinesFieldsTemplate(element)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
variableFieldsTemplate(element) {
|
|
144
|
+
return `
|
|
145
|
+
<hr class="my-3">
|
|
146
|
+
${this.selectField("Variable", "variable_key", element.variable_key, this.scalarVariableOptions())}
|
|
147
|
+
${this.textField("Prefix", "prefix", element.prefix, { maxlength: PREFIX_MAX_LENGTH })}
|
|
148
|
+
${this.textField("Suffix", "suffix", element.suffix, { maxlength: SUFFIX_MAX_LENGTH })}
|
|
149
|
+
${this.textField("Fallback (if empty)", "fallback", element.fallback, { maxlength: FALLBACK_MAX_LENGTH })}
|
|
150
|
+
<hr class="my-3">
|
|
151
|
+
${this.styleFieldsTemplate(element.styles || {})}
|
|
152
|
+
`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
textFieldsTemplate(element) {
|
|
156
|
+
return `
|
|
157
|
+
<hr class="my-3">
|
|
158
|
+
${this.textareaField("Text", "text", element.text, { maxlength: TEXT_MAX_LENGTH })}
|
|
159
|
+
<hr class="my-3">
|
|
160
|
+
${this.styleFieldsTemplate(element.styles || {})}
|
|
161
|
+
`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
styleFieldsTemplate(styles) {
|
|
165
|
+
return `
|
|
166
|
+
<div class="row g-2">
|
|
167
|
+
<div class="col-6">${this.numberField("Font Size (pt)", "styles.font_size_pt", styles.font_size_pt || 10, { min: FONT_SIZE_MIN, max: FONT_SIZE_MAX, step: 1 })}</div>
|
|
168
|
+
<div class="col-6">${this.selectField("Weight", "styles.font_weight", styles.font_weight || "normal", [["Normal", "normal"], ["Bold", "bold"]])}</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="row g-2">
|
|
171
|
+
<div class="col-6">${this.selectField("Align", "styles.text_align", styles.text_align || "left", [["Left", "left"], ["Center", "center"], ["Right", "right"]])}</div>
|
|
172
|
+
<div class="col-6">${this.numberField("Line Height", "styles.line_height", styles.line_height || 1.2, { min: LINE_HEIGHT_MIN, max: LINE_HEIGHT_MAX, step: 0.1 })}</div>
|
|
173
|
+
</div>
|
|
174
|
+
`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
orderLinesFieldsTemplate(element) {
|
|
178
|
+
const config = element.config || {}
|
|
179
|
+
const columns = config.columns || []
|
|
180
|
+
|
|
181
|
+
const columnRows = columns
|
|
182
|
+
.map(
|
|
183
|
+
(column) => `
|
|
184
|
+
<tr>
|
|
185
|
+
<td>${escapeHtml(column.label)}</td>
|
|
186
|
+
<td>${escapeHtml(column.key)}</td>
|
|
187
|
+
<td>${escapeHtml(String(column.width_mm))}mm</td>
|
|
188
|
+
<td>${escapeHtml(column.align)}</td>
|
|
189
|
+
</tr>
|
|
190
|
+
`
|
|
191
|
+
)
|
|
192
|
+
.join("")
|
|
193
|
+
|
|
194
|
+
return `
|
|
195
|
+
<hr class="my-3">
|
|
196
|
+
<div class="row g-2">
|
|
197
|
+
<div class="col-6 d-flex align-items-end">${this.checkboxField("Show header row", "config.show_header", config.show_header !== false)}</div>
|
|
198
|
+
<div class="col-6">${this.numberField("Font Size (pt)", "config.font_size_pt", config.font_size_pt || 8, { min: FONT_SIZE_MIN, max: FONT_SIZE_MAX, step: 1 })}</div>
|
|
199
|
+
</div>
|
|
200
|
+
${this.numberField("Row Spacing (mm)", "config.row_spacing_mm", config.row_spacing_mm != null ? config.row_spacing_mm : 1.0, { min: ROW_SPACING_MIN, max: ROW_SPACING_MAX, step: 0.1 })}
|
|
201
|
+
<label class="form-label mt-2">Columns</label>
|
|
202
|
+
<table class="table table-sm nrb-receipt-columns-table">
|
|
203
|
+
<thead><tr><th>Label</th><th>Key</th><th>Width</th><th>Align</th></tr></thead>
|
|
204
|
+
<tbody>${columnRows}</tbody>
|
|
205
|
+
</table>
|
|
206
|
+
<p class="text-secondary nrb-receipt-hint">Column layout isn’t editable yet — shown for reference.</p>
|
|
207
|
+
`
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
deleteButtonTemplate() {
|
|
211
|
+
return `
|
|
212
|
+
<hr class="my-3">
|
|
213
|
+
<button type="button" class="btn btn-outline-danger w-100" data-action="click->nrb-properties#deleteElement">
|
|
214
|
+
<span class="material-symbols-outlined pe-1">delete</span>Delete Element
|
|
215
|
+
</button>
|
|
216
|
+
`
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Field builders
|
|
220
|
+
|
|
221
|
+
numberField(label, field, value, { min, max, step, valueType = "number" } = {}) {
|
|
222
|
+
const minAttr = min != null ? `min="${min}"` : ""
|
|
223
|
+
const maxAttr = max != null ? `max="${max}"` : ""
|
|
224
|
+
const stepAttr = step != null ? `step="${step}"` : ""
|
|
225
|
+
|
|
226
|
+
return `
|
|
227
|
+
<div class="form-group mb-3">
|
|
228
|
+
<label class="form-label">${escapeHtml(label)}</label>
|
|
229
|
+
<input type="number" class="form-control" value="${escapeHtml(value)}" ${minAttr} ${maxAttr} ${stepAttr}
|
|
230
|
+
data-nrb-properties-field-param="${field}"
|
|
231
|
+
data-nrb-properties-value-type-param="${valueType}"
|
|
232
|
+
data-action="input->nrb-properties#fieldChanged">
|
|
233
|
+
</div>
|
|
234
|
+
`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
textField(label, field, value, { maxlength } = {}) {
|
|
238
|
+
return `
|
|
239
|
+
<div class="form-group mb-3">
|
|
240
|
+
<label class="form-label">${escapeHtml(label)}</label>
|
|
241
|
+
<input type="text" class="form-control" value="${escapeHtml(value)}" maxlength="${maxlength}"
|
|
242
|
+
data-nrb-properties-field-param="${field}"
|
|
243
|
+
data-nrb-properties-value-type-param="string"
|
|
244
|
+
data-action="input->nrb-properties#fieldChanged">
|
|
245
|
+
</div>
|
|
246
|
+
`
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
textareaField(label, field, value, { maxlength } = {}) {
|
|
250
|
+
return `
|
|
251
|
+
<div class="form-group mb-3">
|
|
252
|
+
<label class="form-label">${escapeHtml(label)}</label>
|
|
253
|
+
<textarea class="form-control" rows="3" maxlength="${maxlength}"
|
|
254
|
+
data-nrb-properties-field-param="${field}"
|
|
255
|
+
data-nrb-properties-value-type-param="string"
|
|
256
|
+
data-action="input->nrb-properties#fieldChanged">${escapeHtml(value)}</textarea>
|
|
257
|
+
</div>
|
|
258
|
+
`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
checkboxField(label, field, checked) {
|
|
262
|
+
return `
|
|
263
|
+
<div class="form-check form-switch mb-3">
|
|
264
|
+
<input type="checkbox" class="form-check-input" ${checked ? "checked" : ""}
|
|
265
|
+
data-nrb-properties-field-param="${field}"
|
|
266
|
+
data-nrb-properties-value-type-param="boolean"
|
|
267
|
+
data-action="change->nrb-properties#fieldChanged">
|
|
268
|
+
<label class="form-check-label">${escapeHtml(label)}</label>
|
|
269
|
+
</div>
|
|
270
|
+
`
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
selectField(label, field, selected, options) {
|
|
274
|
+
const optionTags = options
|
|
275
|
+
.map(
|
|
276
|
+
([optLabel, optValue]) =>
|
|
277
|
+
`<option value="${escapeHtml(optValue)}" ${optValue === selected ? "selected" : ""}>${escapeHtml(optLabel)}</option>`
|
|
278
|
+
)
|
|
279
|
+
.join("")
|
|
280
|
+
|
|
281
|
+
return `
|
|
282
|
+
<div class="form-group mb-3">
|
|
283
|
+
<label class="form-label">${escapeHtml(label)}</label>
|
|
284
|
+
<select class="form-select"
|
|
285
|
+
data-nrb-properties-field-param="${field}"
|
|
286
|
+
data-nrb-properties-value-type-param="string"
|
|
287
|
+
data-action="change->nrb-properties#fieldChanged">
|
|
288
|
+
${optionTags}
|
|
289
|
+
</select>
|
|
290
|
+
</div>
|
|
291
|
+
`
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
scalarVariableOptions() {
|
|
295
|
+
return Object.entries(this.scalarVariablesValue).map(([key, label]) => [label, key])
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/* nti_receipt_builder — builder and print surfaces.
|
|
2
|
+
Assumes Tabler 1.x is present in the host layout for card/button/form chrome;
|
|
3
|
+
everything receipt-specific is defined here.
|
|
4
|
+
|
|
5
|
+
Geometry is deliberately absent from the shared element classes: position and size
|
|
6
|
+
always arrive inline from the builder JS or the print presenters, so builder-only
|
|
7
|
+
affordances stay scoped under `.nrb-receipt-paper-elements` and never leak into the
|
|
8
|
+
preview/print render partials that reuse the same bare class names. */
|
|
9
|
+
|
|
10
|
+
/* Theming: set --nrb-accent (and --nrb-accent-rgb for the translucent fills) to tint the
|
|
11
|
+
builder's selection and palette affordances. Both fall back to Tabler's primary. */
|
|
12
|
+
|
|
13
|
+
/* ---- Page header shared by the editor and preview pages ---- */
|
|
14
|
+
.nrb-receipt-topbar {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
gap: 1rem;
|
|
19
|
+
flex-wrap: wrap;
|
|
20
|
+
margin-bottom: 1.25rem;
|
|
21
|
+
}
|
|
22
|
+
.nrb-receipt-topbar-name {
|
|
23
|
+
font-size: 1.3rem; font-weight: 800;
|
|
24
|
+
letter-spacing: -0.01em; color: var(--tblr-dark);
|
|
25
|
+
margin-right: 0.75rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ---- 3-column editor layout: settings+elements / canvas / properties ---- */
|
|
29
|
+
.nrb-receipt-builder {
|
|
30
|
+
display: grid;
|
|
31
|
+
grid-template-columns: minmax(0, 18rem) minmax(0, 1fr) minmax(0, 19rem);
|
|
32
|
+
gap: 1.25rem;
|
|
33
|
+
align-items: start;
|
|
34
|
+
}
|
|
35
|
+
.nrb-receipt-builder-side { display: flex; flex-direction: column; gap: 1.25rem; }
|
|
36
|
+
@media (max-width: 1200px) {
|
|
37
|
+
.nrb-receipt-builder { grid-template-columns: 1fr; }
|
|
38
|
+
.nrb-receipt-builder-side { flex-direction: row; flex-wrap: wrap; }
|
|
39
|
+
.nrb-receipt-builder-side > .nrb-receipt-panel { flex: 1 1 16rem; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ---- Shared glass-panel chrome for the editor's 4 panels ---- */
|
|
43
|
+
.nrb-receipt-panel {
|
|
44
|
+
position: relative;
|
|
45
|
+
border-radius: var(--tblr-card-border-radius, 1.25rem);
|
|
46
|
+
padding: 1.25rem;
|
|
47
|
+
background: #fff;
|
|
48
|
+
border: 1px solid rgba(31, 41, 55, 0.06);
|
|
49
|
+
box-shadow: 0 1.25rem 2.5rem -1.5rem rgba(31, 41, 55, 0.25);
|
|
50
|
+
}
|
|
51
|
+
.nrb-receipt-panel .card-title { margin-bottom: 0.85rem; }
|
|
52
|
+
.nrb-receipt-panel--canvas { display: flex; flex-direction: column; gap: 1rem; }
|
|
53
|
+
.nrb-receipt-hint { font-size: 0.82rem; margin: 0.5rem 0 0; }
|
|
54
|
+
|
|
55
|
+
/* ---- Elements palette (draggable variable/block buttons) ---- */
|
|
56
|
+
.nrb-receipt-palette-group + .nrb-receipt-palette-group { margin-top: 1.25rem; }
|
|
57
|
+
.nrb-receipt-palette-label {
|
|
58
|
+
display: block;
|
|
59
|
+
font-size: 0.7rem; font-weight: 700;
|
|
60
|
+
letter-spacing: 0.05em; text-transform: uppercase;
|
|
61
|
+
color: var(--tblr-secondary);
|
|
62
|
+
margin-bottom: 0.6rem;
|
|
63
|
+
}
|
|
64
|
+
.nrb-receipt-palette { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
65
|
+
.nrb-receipt-palette-btn {
|
|
66
|
+
display: inline-flex; align-items: center; gap: 0.4rem;
|
|
67
|
+
font-size: 0.8rem; font-weight: 600;
|
|
68
|
+
color: var(--tblr-dark);
|
|
69
|
+
background: rgba(var(--nrb-accent-rgb, var(--tblr-primary-rgb)), 0.07);
|
|
70
|
+
border: 1px solid rgba(var(--nrb-accent-rgb, var(--tblr-primary-rgb)), 0.15);
|
|
71
|
+
border-radius: 0.8rem;
|
|
72
|
+
padding: 0.4rem 0.7rem;
|
|
73
|
+
cursor: grab;
|
|
74
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
|
|
75
|
+
}
|
|
76
|
+
.nrb-receipt-palette-btn .material-symbols-outlined {
|
|
77
|
+
font-size: 18px;
|
|
78
|
+
color: var(--nrb-accent, var(--tblr-primary));
|
|
79
|
+
}
|
|
80
|
+
.nrb-receipt-palette-btn:hover {
|
|
81
|
+
transform: translateY(-1px);
|
|
82
|
+
background: rgba(var(--nrb-accent-rgb, var(--tblr-primary-rgb)), 0.14);
|
|
83
|
+
box-shadow: 0 0.5rem 1rem -0.6rem rgba(var(--nrb-accent-rgb, var(--tblr-primary-rgb)), 0.6);
|
|
84
|
+
}
|
|
85
|
+
.nrb-receipt-palette-btn:active { cursor: grabbing; }
|
|
86
|
+
|
|
87
|
+
/* ---- Canvas toolbar (title + zoom control) ---- */
|
|
88
|
+
.nrb-receipt-toolbar {
|
|
89
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
90
|
+
gap: 1rem; flex-wrap: wrap;
|
|
91
|
+
}
|
|
92
|
+
.nrb-receipt-zoom { display: flex; align-items: center; gap: 0.5rem; color: var(--tblr-secondary); }
|
|
93
|
+
.nrb-receipt-zoom input[type="range"] { width: 8rem; }
|
|
94
|
+
.nrb-receipt-zoom-label {
|
|
95
|
+
font-size: 0.8rem; font-weight: 700;
|
|
96
|
+
color: var(--tblr-dark);
|
|
97
|
+
min-width: 2.5rem; text-align: right;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ---- Scrollable canvas viewport + the builder-only paper surface.
|
|
101
|
+
`.nrb-receipt-paper-render` (preview/print) is a separate class —
|
|
102
|
+
the builder's `.nrb-receipt-paper` never appears in rendered output. ---- */
|
|
103
|
+
.nrb-receipt-viewport {
|
|
104
|
+
flex: 1 1 auto;
|
|
105
|
+
min-height: 28rem;
|
|
106
|
+
max-height: 70vh;
|
|
107
|
+
overflow: auto;
|
|
108
|
+
padding: 2rem;
|
|
109
|
+
background: rgba(31, 41, 55, 0.03);
|
|
110
|
+
border: 1px solid rgba(31, 41, 55, 0.06);
|
|
111
|
+
border-radius: 1rem;
|
|
112
|
+
}
|
|
113
|
+
.nrb-receipt-paper {
|
|
114
|
+
position: relative;
|
|
115
|
+
margin: 0 auto;
|
|
116
|
+
background: #fff;
|
|
117
|
+
border: 1px solid rgba(31, 41, 55, 0.1);
|
|
118
|
+
box-shadow: 0 1.25rem 2.5rem -1.5rem rgba(31, 41, 55, 0.3);
|
|
119
|
+
}
|
|
120
|
+
/* margin guides: a visual-only inset overlay, never intercepts pointer events */
|
|
121
|
+
.nrb-receipt-paper-margins {
|
|
122
|
+
position: absolute;
|
|
123
|
+
border: 1px dashed rgba(var(--tblr-primary-rgb), 0.35);
|
|
124
|
+
pointer-events: none;
|
|
125
|
+
}
|
|
126
|
+
.nrb-receipt-paper-elements { position: absolute; inset: 0; }
|
|
127
|
+
|
|
128
|
+
/* ---- Canvas element — base rule is shared with the render/print partials,
|
|
129
|
+
so it must stay geometry-agnostic (position/left/top/width/height
|
|
130
|
+
are always supplied inline, either by receipt_canvas_controller.js
|
|
131
|
+
or by NtiReceiptBuilder::Presenters::Element#css_style / paper_style). ---- */
|
|
132
|
+
.nrb-receipt-element {
|
|
133
|
+
position: absolute;
|
|
134
|
+
box-sizing: border-box;
|
|
135
|
+
overflow: hidden;
|
|
136
|
+
word-break: break-word;
|
|
137
|
+
}
|
|
138
|
+
.nrb-receipt-element-body { width: 100%; height: 100%; overflow: hidden; pointer-events: none; }
|
|
139
|
+
|
|
140
|
+
/* Builder-only interactive affordances — scoped so they never leak into
|
|
141
|
+
the preview/print pages, which reuse the bare `.nrb-receipt-element`. */
|
|
142
|
+
.nrb-receipt-paper-elements .nrb-receipt-element {
|
|
143
|
+
cursor: move;
|
|
144
|
+
touch-action: none;
|
|
145
|
+
user-select: none;
|
|
146
|
+
transition: outline-color 0.15s ease;
|
|
147
|
+
}
|
|
148
|
+
.nrb-receipt-paper-elements .nrb-receipt-element:hover {
|
|
149
|
+
outline: 1px dashed rgba(var(--tblr-primary-rgb), 0.4);
|
|
150
|
+
outline-offset: 1px;
|
|
151
|
+
}
|
|
152
|
+
.nrb-receipt-paper-elements .nrb-receipt-element--selected {
|
|
153
|
+
outline: 2px solid var(--nrb-accent, var(--tblr-primary));
|
|
154
|
+
outline-offset: 1px;
|
|
155
|
+
}
|
|
156
|
+
.nrb-receipt-paper-elements .nrb-receipt-element--hidden {
|
|
157
|
+
opacity: 0.4;
|
|
158
|
+
outline: 1px dashed rgba(31, 41, 55, 0.35);
|
|
159
|
+
outline-offset: 1px;
|
|
160
|
+
}
|
|
161
|
+
.nrb-receipt-paper-elements .nrb-receipt-element-dragging,
|
|
162
|
+
.nrb-receipt-paper-elements .nrb-receipt-element-resizing {
|
|
163
|
+
box-shadow: 0 0.75rem 1.5rem -0.75rem rgba(31, 41, 55, 0.45);
|
|
164
|
+
cursor: grabbing;
|
|
165
|
+
}
|
|
166
|
+
.nrb-receipt-resize-handle {
|
|
167
|
+
position: absolute;
|
|
168
|
+
right: -5px; bottom: -5px;
|
|
169
|
+
width: 0.7rem; height: 0.7rem;
|
|
170
|
+
border-radius: 999px;
|
|
171
|
+
background: var(--nrb-accent, var(--tblr-primary));
|
|
172
|
+
border: 2px solid #fff;
|
|
173
|
+
box-shadow: 0 0.15rem 0.4rem rgba(31, 41, 55, 0.35);
|
|
174
|
+
cursor: nwse-resize;
|
|
175
|
+
touch-action: none;
|
|
176
|
+
opacity: 0;
|
|
177
|
+
transition: opacity 0.15s ease;
|
|
178
|
+
}
|
|
179
|
+
.nrb-receipt-paper-elements .nrb-receipt-element:hover .nrb-receipt-resize-handle,
|
|
180
|
+
.nrb-receipt-paper-elements .nrb-receipt-element--selected .nrb-receipt-resize-handle {
|
|
181
|
+
opacity: 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ---- Order-lines block — shared between the builder placeholder and the
|
|
185
|
+
real render partial; `-empty` only ever appears in rendered output. ---- */
|
|
186
|
+
.nrb-receipt-order-lines { display: block; width: 100%; }
|
|
187
|
+
/* `table-layout: fixed` + percentage <col> widths is what makes a row span the
|
|
188
|
+
element edge-to-edge; row height comes from the inline line-height so rows
|
|
189
|
+
sit directly under one another instead of in tall centred boxes. */
|
|
190
|
+
.nrb-receipt-order-lines-table {
|
|
191
|
+
width: 100%;
|
|
192
|
+
table-layout: fixed;
|
|
193
|
+
border-collapse: collapse;
|
|
194
|
+
}
|
|
195
|
+
/* Cells wrap rather than truncate: a receipt that hides half a product name is worse than one
|
|
196
|
+
that runs to a second line. `overflow-wrap: break-word` only splits a word that cannot fit its
|
|
197
|
+
column on its own — an unbroken SKU in a narrow column — so ordinary text still breaks at
|
|
198
|
+
spaces. Matches `.nrb-receipt-element`, which has wrapped all along. */
|
|
199
|
+
.nrb-receipt-order-lines-table th,
|
|
200
|
+
.nrb-receipt-order-lines-table td {
|
|
201
|
+
padding: 0;
|
|
202
|
+
white-space: normal;
|
|
203
|
+
overflow-wrap: break-word;
|
|
204
|
+
vertical-align: top;
|
|
205
|
+
}
|
|
206
|
+
.nrb-receipt-order-lines-header th {
|
|
207
|
+
font-weight: 700;
|
|
208
|
+
border-bottom: 1px solid rgba(31, 41, 55, 0.18);
|
|
209
|
+
}
|
|
210
|
+
.nrb-receipt-order-lines-empty td { text-align: center; }
|
|
211
|
+
|
|
212
|
+
/* ---- Properties panel read-only column summary table ---- */
|
|
213
|
+
.nrb-receipt-columns-table { font-size: 0.8rem; }
|
|
214
|
+
.nrb-receipt-columns-table th {
|
|
215
|
+
color: var(--tblr-secondary);
|
|
216
|
+
font-weight: 700; text-transform: uppercase;
|
|
217
|
+
font-size: 0.68rem; letter-spacing: 0.04em;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* ---- Preview / print. `.nrb-receipt-paper-render`'s on-screen elevation
|
|
221
|
+
(margin/border/box-shadow) is reset to nothing by print.html.erb's
|
|
222
|
+
inline <style>, since the printed page must be the bare receipt. ---- */
|
|
223
|
+
.nrb-receipt-preview-frame {
|
|
224
|
+
padding: 2rem;
|
|
225
|
+
background: rgba(31, 41, 55, 0.03);
|
|
226
|
+
border-radius: 1.25rem;
|
|
227
|
+
overflow-x: auto;
|
|
228
|
+
}
|
|
229
|
+
.nrb-receipt-paper-render {
|
|
230
|
+
position: relative;
|
|
231
|
+
margin: 0 auto;
|
|
232
|
+
background: #fff;
|
|
233
|
+
border: 1px solid rgba(31, 41, 55, 0.08);
|
|
234
|
+
box-shadow: 0 1.25rem 2.5rem -1.5rem rgba(31, 41, 55, 0.3);
|
|
235
|
+
}
|
|
236
|
+
.nrb-receipt-overflow-banner { margin-bottom: 1.25rem; }
|
|
237
|
+
.nrb-receipt-preview-modal .nrb-receipt-preview-frame { max-height: 65vh; overflow-y: auto; }
|
|
238
|
+
|