nti_receipt_builder 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4493131823c3d523ede3d5e0c57b77caf81245f191dca1019ba5536cdf1dbc55
4
- data.tar.gz: ce594bac64cd068754246e4cf62b90eed741e87c3dc5224dcdd570da267616db
3
+ metadata.gz: 0b3bc19f642f010633b3d388d30073da350288c55ba6b1a0cd23f83bade30302
4
+ data.tar.gz: 45c3e8238d843ed65013872971650cbc741c86b5f25e3f08aafaf03360ee4268
5
5
  SHA512:
6
- metadata.gz: e8b7e1012da9720f4bba538fd3363a93fe4d349c75f6fd8de4c7d66c5dbc094c08b62603e643ebd19579494af386ed9931e9551dab09193cf2becf610006bbce
7
- data.tar.gz: 916a9a7b7f8d7101e663227821564cd5d9391352189c95e465e16d5bd41069cbbcef1cfc915c17dcd481a70f7aac3e7ab37112619439b9b4c33adbc16ec5f61b
6
+ metadata.gz: e03c4aaf3e056d25fd714f2035a273a2f179797d634341cae8e1cef184628f0037175690704c2242edb90782b59b3585ed26273ff280ee93f39dbefa9cdbedb8
7
+ data.tar.gz: d309119820ba6c10419ee8460498378e12619da854a3ab941f431d0693adb4ba4294795b67d929ac121d53f2cc10118c3846d12f8dad515a9d49e12e18e7982e
data/CHANGELOG.md CHANGED
@@ -1,8 +1,52 @@
1
1
  # Changelog
2
2
 
3
- ## [0.1.0] - 2026-08-02
3
+ ## [0.1.2] - 2026-08-13
4
+
5
+ - Paper geometry on the new-template page now reacts to the form. The page mounts `nrb-builder`
6
+ and targets the paper size, dimension, orientation and height mode fields, so the width and
7
+ height shown follow the selected preset instead of staying at whatever the model was
8
+ initialized with. `edit` already had this wiring.
9
+ - Orientation now rewrites the dimensions rather than only the canvas: `settingsChanged` puts the
10
+ larger value on width for `landscape` and on height for `portrait`, and writes both back to the
11
+ inputs. Previously the pair was passed through untouched, so switching orientation left the
12
+ numbers — and the saved record — describing the other orientation.
13
+ - Saved preview opens in a modal over the editor instead of a new tab.
14
+ `templates/preview.html.erb` is replaced by `templates/preview.turbo_stream.erb`, and the
15
+ preview link posts for a Turbo Stream. **Hosts overriding `templates/preview.html.erb` must
16
+ move that override to the Turbo Stream view.**
17
+ - The Print button was the deleted preview page's only appearance, so nothing in the gem's views
18
+ links to `print` now. The action, route and `templates/print.html.erb` are unchanged — hosts
19
+ that need the button should add their own link to the `print` action.
20
+ - Renamed the two preview affordances to say which state they render: "Live Preview" is now
21
+ "Draft Preview" (unsaved form state) and "Preview" is now "Saved Preview" (the persisted
22
+ record).
23
+ - Canvas zoom in and out are real controls. Both glyphs were inert `<span>`s; they are now
24
+ buttons wired to `zoomIn`/`zoomOut`, and all three entry points share `setZoom`, which clamps
25
+ to 0.5–2.0 in 0.1 steps and syncs the range input back to the value.
26
+ - Removed the canvas viewport's `max-height: 70vh` and let the panels size to content, so a tall
27
+ paper scrolls the page instead of being clipped inside the viewport.
28
+
29
+ ## [0.1.1] - 2026-08-04
4
30
 
5
- Initial release, extracted from SalesWiz's receipt builder.
31
+ - Fixed layout edits being silently discarded on save. Serialization hung off
32
+ `turbo:submit-start`, which Turbo dispatches *after* the `FormSubmission` constructor has
33
+ snapshotted the request body, so the server re-saved the layout the page was rendered with.
34
+ The hidden field is now written from `layoutValueChanged` — every add, patch, move, resize and
35
+ delete reassigns `layoutValue`, so it can't go stale — with a `formdata` handler as a
36
+ submit-time backstop for a layout mutated in place.
37
+ - Removed `Elements::MAX_ORDER_LINE_COLUMNS`. `LayoutValidator` now enforces only the lower
38
+ bound, since every column key must already be one of the host's declared collection columns and
39
+ duplicates are rejected — the host's own vocabulary is the ceiling. A host whose collection is
40
+ wider than six columns is no longer rejected.
41
+ - Order line table cells wrap instead of truncating. `overflow-wrap: break-word` splits only a
42
+ word too long for its column on its own, so ordinary text still breaks at spaces.
43
+ - Draft preview no longer trips a host's default-template uniqueness rule. The draft is built
44
+ with `dup`, which copied `is_default`, so the unsaved copy collided with the record it came
45
+ from and preview failed on exactly the templates most likely to be previewed. The flag is now
46
+ cleared on the draft; nothing in the layout or geometry reads it, so the render is unaffected.
47
+ - Draft preview modal widened from `modal-lg` to `modal-xl`.
48
+
49
+ ## [0.1.0] - 2026-08-02
6
50
 
7
51
  - `NtiReceiptBuilder::Variables` — host-declared receipt vocabulary. `MAPPINGS` supplies keys
8
52
  and value methods; label, formatting type and preview sample are derived and overridable per
data/README.md CHANGED
@@ -150,7 +150,7 @@ that omits it raises `NotImplementedError` instead of running an unscoped query.
150
150
 
151
151
  The concern provides `new`, `create`, `edit`, `update`, `destroy`, `preview`,
152
152
  `render_preview`, `print` and `set_default`. Define any of them in your controller to take
153
- over — a method on the including class wins. SalesWiz overrides `destroy`, for instance, to
153
+ over — a method on the including class wins. Rails app overrides `destroy`, for instance, to
154
154
  archive rather than delete. The navigation hooks `after_create_path`, `after_destroy_path` and
155
155
  `after_set_default_path` are overridable too, and worth overriding if you have named routes.
156
156
 
@@ -233,17 +233,32 @@ export default class extends Controller {
233
233
  // Settings panel reactivity
234
234
 
235
235
  settingsChanged() {
236
- if (!this.hasNrbCanvasOutlet) return
237
- const canvas = this.nrbCanvasOutlet
238
-
239
- if (this.hasWidthInputTarget) {
240
- const value = parseFloat(this.widthInputTarget.value)
241
- if (!Number.isNaN(value)) canvas.widthMmValue = value
242
- }
243
- if (this.hasHeightInputTarget) {
244
- const value = parseFloat(this.heightInputTarget.value)
245
- if (!Number.isNaN(value)) canvas.heightMmValue = value
236
+ const canvas = this.hasNrbCanvasOutlet ? this.nrbCanvasOutlet : null
237
+
238
+ const widthInput = this.hasWidthInputTarget ? parseFloat(this.widthInputTarget.value) : NaN
239
+ const heightInput = this.hasHeightInputTarget ? parseFloat(this.heightInputTarget.value) : NaN
240
+ const orientation = this.hasOrientationSelectTarget ? this.orientationSelectTarget.value : "portrait"
241
+
242
+ let widthValue = widthInput
243
+ let heightValue = heightInput
244
+
245
+ if (!Number.isNaN(widthValue) && !Number.isNaN(heightValue)) {
246
+ if (orientation === "landscape") {
247
+ widthValue = Math.max(widthInput, heightInput)
248
+ heightValue = Math.min(widthInput, heightInput)
249
+ } else {
250
+ widthValue = Math.min(widthInput, heightInput)
251
+ heightValue = Math.max(widthInput, heightInput)
252
+ }
246
253
  }
254
+
255
+ if (this.hasWidthInputTarget && !Number.isNaN(widthValue)) this.widthInputTarget.value = String(widthValue)
256
+ if (this.hasHeightInputTarget && !Number.isNaN(heightValue)) this.heightInputTarget.value = String(heightValue)
257
+
258
+ if (!canvas) return
259
+
260
+ if (this.hasWidthInputTarget && !Number.isNaN(widthValue)) canvas.widthMmValue = widthValue
261
+ if (this.hasHeightInputTarget && !Number.isNaN(heightValue)) canvas.heightMmValue = heightValue
247
262
  if (this.hasHeightModeSelectTarget) canvas.heightModeValue = this.heightModeSelectTarget.value
248
263
  if (this.hasMarginTopInputTarget) canvas.marginTopMmValue = parseFloat(this.marginTopInputTarget.value) || 0
249
264
  if (this.hasMarginRightInputTarget) canvas.marginRightMmValue = parseFloat(this.marginRightInputTarget.value) || 0
@@ -2,6 +2,9 @@ import { Controller } from "@hotwired/stimulus"
2
2
 
3
3
  const PX_PER_MM_AT_100 = 96 / 25.4
4
4
  const MIN_ELEMENT_DIMENSION_MM = 1.0
5
+ const MIN_ZOOM = 0.5
6
+ const MAX_ZOOM = 2.0
7
+ const ZOOM_STEP = 0.1
5
8
  // Keep in sync with NtiReceiptBuilder::Presenters::Collection.
6
9
  const MM_PER_PT = 0.3528
7
10
  const LINE_HEIGHT_FACTOR = 1.25
@@ -99,11 +102,25 @@ export default class extends Controller {
99
102
  }
100
103
 
101
104
  zoomChanged(event) {
102
- this.zoomValue = parseFloat(event.target.value)
105
+ this.setZoom(parseFloat(event.target.value))
106
+ }
107
+
108
+ zoomIn() {
109
+ this.setZoom(this.zoomValue + ZOOM_STEP)
110
+ }
111
+
112
+ zoomOut() {
113
+ this.setZoom(this.zoomValue - ZOOM_STEP)
114
+ }
115
+
116
+ setZoom(value) {
117
+ const nextValue = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, value))
118
+ this.zoomValue = Number(nextValue.toFixed(1))
103
119
  }
104
120
 
105
121
  zoomValueChanged(value) {
106
122
  if (this.hasZoomLabelTarget) this.zoomLabelTarget.textContent = `${Math.round(value * 100)}%`
123
+ if (this.hasZoomRangeTarget) this.zoomRangeTarget.value = String(value)
107
124
  this.applyZoom()
108
125
  }
109
126
 
@@ -47,9 +47,10 @@
47
47
  background: #fff;
48
48
  border: 1px solid rgba(31, 41, 55, 0.06);
49
49
  box-shadow: 0 1.25rem 2.5rem -1.5rem rgba(31, 41, 55, 0.25);
50
+ height: auto;
50
51
  }
51
52
  .nrb-receipt-panel .card-title { margin-bottom: 0.85rem; }
52
- .nrb-receipt-panel--canvas { display: flex; flex-direction: column; gap: 1rem; }
53
+ .nrb-receipt-panel--canvas { display: flex; flex-direction: column; gap: 1rem; height: auto; }
53
54
  .nrb-receipt-hint { font-size: 0.82rem; margin: 0.5rem 0 0; }
54
55
 
55
56
  /* ---- Elements palette (draggable variable/block buttons) ---- */
@@ -103,7 +104,6 @@
103
104
  .nrb-receipt-viewport {
104
105
  flex: 1 1 auto;
105
106
  min-height: 28rem;
106
- max-height: 70vh;
107
107
  overflow: auto;
108
108
  padding: 2rem;
109
109
  background: rgba(31, 41, 55, 0.03);
@@ -236,3 +236,12 @@
236
236
  .nrb-receipt-overflow-banner { margin-bottom: 1.25rem; }
237
237
  .nrb-receipt-preview-modal .nrb-receipt-preview-frame { max-height: 65vh; overflow-y: auto; }
238
238
 
239
+ .btn-xs {
240
+ padding: 0.15rem 0.4rem;
241
+ border:none;
242
+ box-shadow: none;
243
+ }
244
+
245
+ .material-symbols-outlined{
246
+ font-size: 18px;
247
+ }
@@ -84,6 +84,11 @@ module NtiReceiptBuilder
84
84
 
85
85
  @draft = @receipt_template.dup
86
86
  @draft.assign_attributes(form.attributes.except(:lock_version))
87
+ # A preview is never anyone's default. `dup` copies the flag, so a host enforcing one
88
+ # default per owner saw the unsaved draft collide with the very record it was copied from,
89
+ # and Live Preview failed on precisely the templates most likely to be previewed. Clearing
90
+ # it cannot change the render — nothing in the layout or geometry reads it.
91
+ @draft.is_default = false
87
92
 
88
93
  if !form.valid?
89
94
  @render_result = nil
@@ -12,11 +12,17 @@
12
12
  <div class="nrb-receipt-toolbar">
13
13
  <h3 class="card-title mb-0">Canvas</h3>
14
14
  <div class="nrb-receipt-zoom">
15
- <span class="material-symbols-outlined">zoom_out</span>
15
+ <button type="button" class="btn btn-xs" aria-label="Zoom out"
16
+ data-action="click->nrb-canvas#zoomOut">
17
+ <span class="material-symbols-outlined" aria-hidden="true">zoom_out</span>
18
+ </button>
16
19
  <input type="range" min="0.5" max="2" step="0.1" value="1"
17
20
  data-nrb-canvas-target="zoomRange"
18
21
  data-action="input->nrb-canvas#zoomChanged">
19
- <span class="material-symbols-outlined">zoom_in</span>
22
+ <button type="button" class="btn btn-xs" aria-label="Zoom in"
23
+ data-action="click->nrb-canvas#zoomIn">
24
+ <span class="material-symbols-outlined" aria-hidden="true">zoom_in</span>
25
+ </button>
20
26
  <span class="nrb-receipt-zoom-label" data-nrb-canvas-target="zoomLabel">100%</span>
21
27
  </div>
22
28
  </div>
@@ -26,11 +26,11 @@
26
26
  <span class="text-secondary"><%= @receipt_template.paper_width_mm %>mm × <%= @receipt_template.paper_height_mm %>mm</span>
27
27
  </div>
28
28
  <div class="d-flex justify-content-end gap-2">
29
- <%= f.button 'Live Preview', type: 'submit', class: 'btn btn-outline-secondary',
29
+ <%= f.button 'Draft Preview', type: 'submit', class: 'btn btn-outline-secondary',
30
30
  formaction: url_for(action: :render_preview, id: @receipt_template.id), formmethod: :post %>
31
31
  <%= link_to url_for(action: :preview, id: @receipt_template.id),
32
- class: 'btn btn-outline-secondary', target: '_blank', rel: 'noopener' do %>
33
- <span class="material-symbols-outlined pe-1">visibility</span>Preview
32
+ class: 'btn btn-outline-secondary', data: { turbo_stream: true, turbo_method: :post } do %>
33
+ <span class="material-symbols-outlined pe-1">visibility</span>Saved Preview
34
34
  <% end %>
35
35
  <%= link_to 'Back to Templates', url_for(action: :index), class: 'btn btn-secondary' %>
36
36
  <%= f.submit 'Save', class: 'btn btn-primary' %>
@@ -1,6 +1,10 @@
1
1
  <%= content_for :title, 'New Receipt Template' %>
2
2
 
3
- <%= form_with model: @receipt_template, url: url_for(action: :create), scope: :receipt_template do |f| %>
3
+ <%= form_with model: @receipt_template, url: url_for(action: :create), scope: :receipt_template,
4
+ data: {
5
+ controller: 'nrb-builder',
6
+ nrb_builder_presets_value: NtiReceiptBuilder::Template::PAPER_PRESETS.to_json
7
+ } do |f| %>
4
8
  <h3 class="card-title">Receipt Template</h3>
5
9
  <div id="errors">
6
10
  <% if @errors.present? %>
@@ -20,7 +24,11 @@
20
24
  <%= f.select :paper_size_key,
21
25
  NtiReceiptBuilder::Template::PAPER_PRESETS.map { |key, preset| [preset[:label], key] },
22
26
  {},
23
- class: 'form-select' %>
27
+ class: 'form-select',
28
+ data: {
29
+ nrb_builder_target: 'paperSizeSelect',
30
+ action: 'change->nrb-builder#paperPresetChanged'
31
+ } %>
24
32
  </div>
25
33
 
26
34
  <div class="row g-3">
@@ -30,7 +38,11 @@
30
38
  <%= f.number_field :paper_width_mm, step: 0.1,
31
39
  min: NtiReceiptBuilder::Template::MIN_PAPER_DIMENSION_MM,
32
40
  max: NtiReceiptBuilder::Template::MAX_PAPER_DIMENSION_MM,
33
- class: 'form-control' %>
41
+ class: 'form-control',
42
+ data: {
43
+ nrb_builder_target: 'widthInput',
44
+ action: 'input->nrb-builder#settingsChanged'
45
+ } %>
34
46
  </div>
35
47
  </div>
36
48
  <div class="col">
@@ -39,7 +51,11 @@
39
51
  <%= f.number_field :paper_height_mm, step: 0.1,
40
52
  min: NtiReceiptBuilder::Template::MIN_PAPER_DIMENSION_MM,
41
53
  max: NtiReceiptBuilder::Template::MAX_PAPER_DIMENSION_MM,
42
- class: 'form-control' %>
54
+ class: 'form-control',
55
+ data: {
56
+ nrb_builder_target: 'heightInput',
57
+ action: 'input->nrb-builder#settingsChanged'
58
+ } %>
43
59
  </div>
44
60
  </div>
45
61
  </div>
@@ -51,7 +67,11 @@
51
67
  <%= f.select :orientation,
52
68
  NtiReceiptBuilder::Template::ORIENTATIONS.map { |value| [value.titleize, value] },
53
69
  {},
54
- class: 'form-select' %>
70
+ class: 'form-select',
71
+ data: {
72
+ nrb_builder_target: 'orientationSelect',
73
+ action: 'change->nrb-builder#settingsChanged'
74
+ } %>
55
75
  </div>
56
76
  </div>
57
77
  <div class="col">
@@ -60,7 +80,11 @@
60
80
  <%= f.select :height_mode,
61
81
  [['Fixed Height', 'fixed'], ['Grows With Content', 'content']],
62
82
  {},
63
- class: 'form-select' %>
83
+ class: 'form-select',
84
+ data: {
85
+ nrb_builder_target: 'heightModeSelect',
86
+ action: 'change->nrb-builder#settingsChanged'
87
+ } %>
64
88
  </div>
65
89
  </div>
66
90
  </div>
@@ -0,0 +1,22 @@
1
+ <%= turbo_stream.open_modal size: 'modal-xl' do %>
2
+ <div class="modal-header">
3
+ <h1 class="modal-title">Saved Preview</h1>
4
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
5
+ </div>
6
+
7
+ <div class="modal-body nrb-receipt-preview-modal">
8
+ <% if @render_result.overflow? %>
9
+ <%= render 'nti_receipt_builder/render/overflow_banner' %>
10
+ <% end %>
11
+
12
+ <div class="nrb-receipt-preview-frame">
13
+ <%= render 'nti_receipt_builder/render/paper',
14
+ receipt_template: @receipt_template,
15
+ render_result: @render_result %>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="modal-footer">
20
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
21
+ </div>
22
+ <% end %>
@@ -1,6 +1,6 @@
1
- <%= turbo_stream.open_modal size: 'modal-lg' do %>
1
+ <%= turbo_stream.open_modal size: 'modal-xl' do %>
2
2
  <div class="modal-header">
3
- <h1 class="modal-title">Live Preview</h1>
3
+ <h1 class="modal-title">Draft Preview</h1>
4
4
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
5
5
  </div>
6
6
  <div class="modal-body nrb-receipt-preview-modal">
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NtiReceiptBuilder
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nti_receipt_builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Den Meralpis
@@ -120,7 +120,7 @@ files:
120
120
  - app/views/nti_receipt_builder/templates/create.turbo_stream.erb
121
121
  - app/views/nti_receipt_builder/templates/edit.html.erb
122
122
  - app/views/nti_receipt_builder/templates/new.html.erb
123
- - app/views/nti_receipt_builder/templates/preview.html.erb
123
+ - app/views/nti_receipt_builder/templates/preview.turbo_stream.erb
124
124
  - app/views/nti_receipt_builder/templates/print.html.erb
125
125
  - app/views/nti_receipt_builder/templates/render_preview.turbo_stream.erb
126
126
  - app/views/nti_receipt_builder/templates/update.turbo_stream.erb
@@ -1,24 +0,0 @@
1
- <% content_for :title, "Preview: #{@receipt_template.name}" %>
2
- <% content_for :cardless, true %>
3
-
4
- <div class="nrb-receipt-topbar">
5
- <div>
6
- <span class="nrb-receipt-topbar-name"><%= @receipt_template.name %></span>
7
- <span class="text-secondary"><%= @receipt_template.paper_width_mm %>mm &times; <%= @receipt_template.paper_height_mm %>mm</span>
8
- </div>
9
- <div class="d-flex justify-content-end gap-2">
10
- <%= link_to url_for(action: :print, id: @receipt_template.id),
11
- class: 'btn btn-outline-secondary', target: '_blank', rel: 'noopener' do %>
12
- <span class="material-symbols-outlined pe-1">print</span>Print
13
- <% end %>
14
- <%= link_to 'Back to Editor', url_for(action: :edit, id: @receipt_template.id), class: 'btn btn-secondary' %>
15
- </div>
16
- </div>
17
-
18
- <% if @render_result.overflow? %>
19
- <%= render 'nti_receipt_builder/render/overflow_banner' %>
20
- <% end %>
21
-
22
- <div class="nrb-receipt-preview-frame">
23
- <%= render 'nti_receipt_builder/render/paper', receipt_template: @receipt_template, render_result: @render_result %>
24
- </div>