@fleetbase/ember-ui 0.3.22 → 0.3.24

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.
Files changed (46) hide show
  1. package/addon/components/layout/header/smart-nav-menu/dropdown.hbs +77 -68
  2. package/addon/components/layout/header/smart-nav-menu/dropdown.js +7 -54
  3. package/addon/components/template-builder/canvas.hbs +23 -0
  4. package/addon/components/template-builder/canvas.js +116 -0
  5. package/addon/components/template-builder/element-renderer.hbs +126 -0
  6. package/addon/components/template-builder/element-renderer.js +398 -0
  7. package/addon/components/template-builder/layers-panel.hbs +99 -0
  8. package/addon/components/template-builder/layers-panel.js +146 -0
  9. package/addon/components/template-builder/properties-panel/field.hbs +7 -0
  10. package/addon/components/template-builder/properties-panel/field.js +9 -0
  11. package/addon/components/template-builder/properties-panel/section.hbs +24 -0
  12. package/addon/components/template-builder/properties-panel/section.js +19 -0
  13. package/addon/components/template-builder/properties-panel.hbs +576 -0
  14. package/addon/components/template-builder/properties-panel.js +413 -0
  15. package/addon/components/template-builder/queries-panel.hbs +84 -0
  16. package/addon/components/template-builder/queries-panel.js +88 -0
  17. package/addon/components/template-builder/query-form.hbs +260 -0
  18. package/addon/components/template-builder/query-form.js +309 -0
  19. package/addon/components/template-builder/toolbar.hbs +134 -0
  20. package/addon/components/template-builder/toolbar.js +106 -0
  21. package/addon/components/template-builder/variable-picker.hbs +210 -0
  22. package/addon/components/template-builder/variable-picker.js +181 -0
  23. package/addon/components/template-builder.hbs +119 -0
  24. package/addon/components/template-builder.js +567 -0
  25. package/addon/helpers/string-starts-with.js +14 -0
  26. package/addon/services/template-builder.js +72 -0
  27. package/addon/styles/addon.css +1 -0
  28. package/addon/styles/components/badge.css +66 -12
  29. package/addon/styles/components/smart-nav-menu.css +35 -29
  30. package/addon/styles/components/template-builder.css +297 -0
  31. package/addon/utils/get-currency.js +1 -1
  32. package/app/components/template-builder/canvas.js +1 -0
  33. package/app/components/template-builder/element-renderer.js +1 -0
  34. package/app/components/template-builder/layers-panel.js +1 -0
  35. package/app/components/template-builder/properties-panel/field.js +1 -0
  36. package/app/components/template-builder/properties-panel/section.js +1 -0
  37. package/app/components/template-builder/properties-panel.js +1 -0
  38. package/app/components/template-builder/queries-panel.js +1 -0
  39. package/app/components/template-builder/query-form.js +1 -0
  40. package/app/components/template-builder/toolbar.js +1 -0
  41. package/app/components/template-builder/variable-picker.js +1 -0
  42. package/app/components/template-builder.js +1 -0
  43. package/app/helpers/string-starts-with.js +1 -0
  44. package/app/services/template-builder.js +1 -0
  45. package/package.json +3 -2
  46. package/tsconfig.declarations.json +8 -8
@@ -2,9 +2,10 @@
2
2
  Layout::Header::SmartNavMenu::Dropdown
3
3
  Phase 2: multi-column card grid with search filter.
4
4
 
5
- Shortcuts are expanded as independent sibling items in the grid (AWS-style).
6
- The JS getter `expandedItems` interleaves parent MenuItems and their
7
- shortcut objects so each appears as its own flat card.
5
+ Shortcuts are already registered as first-class items in the universe
6
+ registry by menu-service.registerHeaderMenuItem(), so @items already
7
+ contains both parent extension items and their shortcut siblings.
8
+ No client-side expansion is needed.
8
9
 
9
10
  Args:
10
11
  @items - Array of MenuItem objects to display
@@ -15,9 +16,13 @@
15
16
  @onQuickPin - Action to pin an item directly from the dropdown
16
17
  @atPinnedLimit - Boolean: true when the bar is full (pin button hidden)
17
18
 
18
- NOTE: Route-based cards use <LinkToExternal> with no click handler.
19
- The dropdown is closed via the routeDidChange listener in smart-nav-menu.js
20
- which sets isMoreOpen = false on every route transition.
19
+ Click-area pattern:
20
+ Each card is wrapped in a `snm-dropdown-card-wrap` div (position: relative).
21
+ The card itself is the <LinkToExternal> (or <a> for onClick items), making
22
+ the entire card surface clickable. The pin button is a sibling of the card
23
+ inside the wrapper, absolutely positioned in the top-right corner so it
24
+ sits above the card link and receives its own clicks without violating the
25
+ no-nested-interactive rule.
21
26
  }}
22
27
  <div
23
28
  class="snm-dropdown snm-dropdown--wide"
@@ -38,7 +43,7 @@
38
43
  </button>
39
44
  </div>
40
45
 
41
- {{! ── Search bar (own row) ────────────────────────────────────────────── }}
46
+ {{! ── Search bar ──────────────────────────────────────────────────────── }}
42
47
  <div class="snm-dropdown-search-bar">
43
48
  <span class="snm-dropdown-search-icon" aria-hidden="true">
44
49
  <FaIcon @icon="magnifying-glass" @size="xs" />
@@ -76,15 +81,15 @@
76
81
  {{else}}
77
82
  {{#each this.filteredItems as |item|}}
78
83
  {{#if item._isShortcut}}
79
- {{! ── Shortcut sibling card (AWS-style flat item) ─────── }}
80
- <div class="snm-dropdown-card" role="presentation">
81
- <div class="snm-dropdown-card-header">
82
- <LinkToExternal
83
- @route={{item.route}}
84
- id={{concat (dasherize (or item.route item.id "sc")) "-dropdown-card"}}
85
- class="snm-dropdown-card-link"
86
- title={{item.title}}
87
- >
84
+ {{! ── Shortcut card ───────────────────────────────────── }}
85
+ <div class="snm-dropdown-card-wrap">
86
+ <LinkToExternal
87
+ @route={{item.route}}
88
+ id={{concat (dasherize (or item.route item.id "sc")) "-dropdown-card"}}
89
+ class="snm-dropdown-card"
90
+ title={{item.title}}
91
+ >
92
+ <div class="snm-dropdown-card-header">
88
93
  <span class="snm-dropdown-card-icon">
89
94
  {{#if item.iconComponent}}
90
95
  {{component (lazy-engine-component item.iconComponent) options=item.iconComponentOptions}}
@@ -92,43 +97,42 @@
92
97
  <FaIcon @icon={{or item.icon "circle-dot"}} @prefix={{item.iconPrefix}} @size="sm" />
93
98
  {{/if}}
94
99
  </span>
95
- {{! Title row: shortcut name + always-visible muted parent attribution }}
96
100
  <span class="snm-dropdown-card-title-group">
97
101
  <span class="snm-dropdown-card-title">{{item.title}}</span>
98
102
  {{#if item._parentTitle}}
99
103
  <span class="snm-dropdown-card-parent-label" aria-label="from {{item._parentTitle}}">· {{item._parentTitle}}</span>
100
104
  {{/if}}
101
105
  </span>
102
- </LinkToExternal>
103
- {{#unless @atPinnedLimit}}
104
- <button
105
- type="button"
106
- class="snm-dropdown-pin-btn"
107
- title="Pin to navigation bar"
108
- aria-label="Pin {{item.title}} to navigation bar"
109
- {{on "click" (fn @onQuickPin item)}}
110
- >
111
- <FaIcon @icon="thumbtack" @size="xs" />
112
- </button>
113
- {{/unless}}
114
- </div>
115
- {{#if item.description}}
116
- <p class="snm-dropdown-card-description">{{item.description}}</p>
117
- {{else if item._parentTitle}}
118
- <p class="snm-dropdown-card-description snm-dropdown-card-description--from"><em>from {{item._parentTitle}}</em></p>
119
- {{/if}}
106
+ </div>
107
+ {{#if item.description}}
108
+ <p class="snm-dropdown-card-description">{{item.description}}</p>
109
+ {{else if item._parentTitle}}
110
+ <p class="snm-dropdown-card-description snm-dropdown-card-description--from"><em>from {{item._parentTitle}}</em></p>
111
+ {{/if}}
112
+ </LinkToExternal>
113
+ {{#unless @atPinnedLimit}}
114
+ <button
115
+ type="button"
116
+ class="snm-dropdown-pin-btn"
117
+ title="Pin to navigation bar"
118
+ aria-label="Pin {{item.title}} to navigation bar"
119
+ {{on "click" (fn @onQuickPin item)}}
120
+ >
121
+ <FaIcon @icon="thumbtack" @size="xs" />
122
+ </button>
123
+ {{/unless}}
120
124
  </div>
121
125
  {{else}}
122
126
  {{! ── Primary extension card ──────────────────────────── }}
123
- <div class="snm-dropdown-card" role="presentation">
124
- <div class="snm-dropdown-card-header">
125
- {{#if item.onClick}}
126
- <a
127
- href="javascript:;"
128
- class="snm-dropdown-card-link"
129
- title={{item.title}}
130
- {{on "click" (fn this.handleItemClick item)}}
131
- >
127
+ <div class="snm-dropdown-card-wrap">
128
+ {{#if item.onClick}}
129
+ <a
130
+ href="javascript:;"
131
+ class="snm-dropdown-card"
132
+ title={{item.title}}
133
+ {{on "click" (fn this.handleItemClick item)}}
134
+ >
135
+ <div class="snm-dropdown-card-header">
132
136
  <span class="snm-dropdown-card-icon">
133
137
  {{#if item.iconComponent}}
134
138
  {{component (lazy-engine-component item.iconComponent) options=item.iconComponentOptions}}
@@ -142,14 +146,19 @@
142
146
  <span class="snm-dropdown-card-parent-label" aria-label="from {{item._parentTitle}}">· {{item._parentTitle}}</span>
143
147
  {{/if}}
144
148
  </span>
145
- </a>
146
- {{else}}
147
- <LinkToExternal
148
- @route={{item.route}}
149
- id={{concat (dasherize (or item.route item.id "nav")) "-dropdown-card"}}
150
- class="snm-dropdown-card-link"
151
- title={{item.title}}
152
- >
149
+ </div>
150
+ {{#if item.description}}
151
+ <p class="snm-dropdown-card-description">{{item.description}}</p>
152
+ {{/if}}
153
+ </a>
154
+ {{else}}
155
+ <LinkToExternal
156
+ @route={{item.route}}
157
+ id={{concat (dasherize (or item.route item.id "nav")) "-dropdown-card"}}
158
+ class="snm-dropdown-card"
159
+ title={{item.title}}
160
+ >
161
+ <div class="snm-dropdown-card-header">
153
162
  <span class="snm-dropdown-card-icon">
154
163
  {{#if item.iconComponent}}
155
164
  {{component (lazy-engine-component item.iconComponent) options=item.iconComponentOptions}}
@@ -163,23 +172,23 @@
163
172
  <span class="snm-dropdown-card-parent-label" aria-label="from {{item._parentTitle}}">· {{item._parentTitle}}</span>
164
173
  {{/if}}
165
174
  </span>
166
- </LinkToExternal>
167
- {{/if}}
168
- {{#unless @atPinnedLimit}}
169
- <button
170
- type="button"
171
- class="snm-dropdown-pin-btn"
172
- title="Pin to navigation bar"
173
- aria-label="Pin {{item.title}} to navigation bar"
174
- {{on "click" (fn @onQuickPin item)}}
175
- >
176
- <FaIcon @icon="thumbtack" @size="xs" />
177
- </button>
178
- {{/unless}}
179
- </div>
180
- {{#if item.description}}
181
- <p class="snm-dropdown-card-description">{{item.description}}</p>
175
+ </div>
176
+ {{#if item.description}}
177
+ <p class="snm-dropdown-card-description">{{item.description}}</p>
178
+ {{/if}}
179
+ </LinkToExternal>
182
180
  {{/if}}
181
+ {{#unless @atPinnedLimit}}
182
+ <button
183
+ type="button"
184
+ class="snm-dropdown-pin-btn"
185
+ title="Pin to navigation bar"
186
+ aria-label="Pin {{item.title}} to navigation bar"
187
+ {{on "click" (fn @onQuickPin item)}}
188
+ >
189
+ <FaIcon @icon="thumbtack" @size="xs" />
190
+ </button>
191
+ {{/unless}}
183
192
  </div>
184
193
  {{/if}}
185
194
  {{/each}}
@@ -1,7 +1,6 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { action } from '@ember/object';
4
- import { dasherize } from '@ember/string';
5
4
  import { isArray } from '@ember/array';
6
5
  import { htmlSafe } from '@ember/template';
7
6
 
@@ -25,62 +24,16 @@ export default class LayoutHeaderSmartNavMenuDropdownComponent extends Component
25
24
  }
26
25
 
27
26
  /**
28
- * Expand every MenuItem's shortcuts array into sibling flat items.
29
- * The resulting array interleaves parent items and their shortcuts in
30
- * registration order, matching the AWS Console pattern.
27
+ * Returns the items array as-is for filtering.
31
28
  *
32
- * Each shortcut is normalised to:
33
- * { title, route, icon, iconPrefix, id, _isShortcut: true, _parentTitle }
29
+ * Shortcuts are already registered as first-class items in the universe
30
+ * registry (with `_isShortcut: true` and `_parentTitle` set) by
31
+ * `menu-service.registerHeaderMenuItem()` at boot time. There is no need
32
+ * to expand `item.shortcuts` here — doing so would produce a duplicate card
33
+ * for every shortcut (one from the registry, one from the expansion).
34
34
  */
35
35
  get expandedItems() {
36
- const items = this.args.items ?? [];
37
- const result = [];
38
- for (const item of items) {
39
- result.push(item);
40
- if (isArray(item.shortcuts)) {
41
- for (const sc of item.shortcuts) {
42
- const scId = sc.id ?? dasherize(item.id + '-sc-' + sc.title);
43
- result.push({
44
- // ── Identity ────────────────────────────────────────
45
- id: scId,
46
- slug: sc.slug ?? scId,
47
- title: sc.title,
48
- text: sc.text ?? sc.title,
49
- label: sc.label ?? sc.title,
50
-
51
- // ── Routing ──────────────────────────────────────────
52
- route: sc.route ?? item.route,
53
- queryParams: sc.queryParams ?? {},
54
- routeParams: sc.routeParams ?? [],
55
-
56
- // ── Icons (full surface) ─────────────────────────────
57
- icon: sc.icon ?? item.icon ?? 'arrow-right',
58
- iconPrefix: sc.iconPrefix ?? item.iconPrefix ?? null,
59
- iconSize: sc.iconSize ?? null,
60
- iconClass: sc.iconClass ?? null,
61
- iconComponent: sc.iconComponent ?? null,
62
- iconComponentOptions: sc.iconComponentOptions ?? {},
63
-
64
- // ── Metadata ─────────────────────────────────────────
65
- description: sc.description ?? null,
66
- tags: isArray(sc.tags) ? sc.tags : isArray(item.tags) ? item.tags : null,
67
-
68
- // ── Behaviour ────────────────────────────────────────
69
- onClick: sc.onClick ?? null,
70
- disabled: sc.disabled ?? false,
71
-
72
- // ── Styling ───────────────────────────────────────────
73
- class: sc.class ?? null,
74
-
75
- // ── Internal flags ────────────────────────────────────
76
- _isShortcut: true,
77
- _parentTitle: item.title,
78
- _parentId: item.id,
79
- });
80
- }
81
- }
82
- }
83
- return result;
36
+ return this.args.items ?? [];
84
37
  }
85
38
 
86
39
  get filteredItems() {
@@ -0,0 +1,23 @@
1
+ {{! Template Builder Canvas }}
2
+ {{! This component is a thin rendering shell. Each ElementRenderer owns its
3
+ own interact.js instance and emits @onMove / @onResize / @onSelect. }}
4
+ <div
5
+ id={{this.canvasId}}
6
+ class="tb-canvas shadow-xl mx-auto select-none"
7
+ style={{this.canvasStyle}}
8
+ {{on "click" this.handleDeselectAll}}
9
+ ...attributes
10
+ >
11
+ {{#each this.elements key="uuid" as |element|}}
12
+ <TemplateBuilder::ElementRenderer
13
+ @element={{element}}
14
+ @isSelected={{eq element.uuid this.selectedUuid}}
15
+ @zoom={{this.zoom}}
16
+ @canvasWidth={{this.canvasWidthPx}}
17
+ @canvasHeight={{this.canvasHeightPx}}
18
+ @onSelect={{this.handleSelectElement}}
19
+ @onMove={{@onMoveElement}}
20
+ @onResize={{@onResizeElement}}
21
+ />
22
+ {{/each}}
23
+ </div>
@@ -0,0 +1,116 @@
1
+ import Component from '@glimmer/component';
2
+ import { action } from '@ember/object';
3
+ import { guidFor } from '@ember/object/internals';
4
+
5
+ /**
6
+ * TemplateBuilderCanvasComponent
7
+ *
8
+ * Renders the document canvas — a pixel-accurate representation of the
9
+ * template page. Each element is rendered by an ElementRenderer component
10
+ * that owns its own interact.js instance, drag/resize behaviour, and
11
+ * selection tap handling.
12
+ *
13
+ * This component is intentionally thin:
14
+ * - Computes canvas dimensions and style from @template
15
+ * - Tracks which element is selected (by uuid primitive)
16
+ * - Handles deselect-all when the canvas background is clicked
17
+ * - Passes @canvasWidth / @canvasHeight to each ElementRenderer so it can
18
+ * clamp drag/resize to the canvas boundary
19
+ *
20
+ * It has no knowledge of interact.js, DOM nodes, or element data mutations.
21
+ *
22
+ * @argument {Object} template - The template object (width, height, unit, background_color, content)
23
+ * @argument {Object} selectedElement - The currently selected element (or null)
24
+ * @argument {Number} zoom - Zoom level (1 = 100%)
25
+ * @argument {Function} onSelectElement - Called with (element) when an element is tapped
26
+ * @argument {Function} onMoveElement - Called with (uuid, { x, y }) after a drag ends
27
+ * @argument {Function} onResizeElement - Called with (uuid, { x, y, width, height }) after a resize ends
28
+ * @argument {Function} onDeselectAll - Called when the canvas background is clicked
29
+ */
30
+ export default class TemplateBuilderCanvasComponent extends Component {
31
+ canvasId = `tb-canvas-${guidFor(this)}`;
32
+
33
+ // No local selection state. The canvas derives selectedUuid from
34
+ // @selectedElement (owned by TemplateBuilderComponent) so that selections
35
+ // made from the layers panel, keyboard shortcuts, or any other source are
36
+ // always reflected here without duplication.
37
+
38
+ // -------------------------------------------------------------------------
39
+ // Canvas dimensions
40
+ // -------------------------------------------------------------------------
41
+
42
+ get zoom() {
43
+ return this.args.zoom ?? 1;
44
+ }
45
+
46
+ get canvasWidthPx() {
47
+ const { template } = this.args;
48
+ if (!template) return 794;
49
+ return this._unitToPx(template.width ?? 210, template.unit ?? 'mm');
50
+ }
51
+
52
+ get canvasHeightPx() {
53
+ const { template } = this.args;
54
+ if (!template) return 1123;
55
+ return this._unitToPx(template.height ?? 297, template.unit ?? 'mm');
56
+ }
57
+
58
+ get canvasStyle() {
59
+ const w = this.canvasWidthPx * this.zoom;
60
+ const h = this.canvasHeightPx * this.zoom;
61
+ const bg = this.args.template?.background_color ?? '#ffffff';
62
+ return `width:${w}px; height:${h}px; background:${bg}; position:relative; overflow:hidden;`;
63
+ }
64
+
65
+ get elements() {
66
+ return this.args.template?.content ?? [];
67
+ }
68
+
69
+ get selectedUuid() {
70
+ return this.args.selectedElement?.uuid ?? null;
71
+ }
72
+
73
+ // -------------------------------------------------------------------------
74
+ // Selection
75
+ // -------------------------------------------------------------------------
76
+
77
+ @action
78
+ handleSelectElement(element) {
79
+ // Delegate entirely to the parent. The parent sets selectedElement,
80
+ // which flows back down as @selectedElement, which drives selectedUuid.
81
+ if (this.args.onSelectElement) {
82
+ this.args.onSelectElement(element);
83
+ }
84
+ }
85
+
86
+ @action
87
+ handleDeselectAll(event) {
88
+ // Only deselect when the user clicks the canvas background directly.
89
+ // If the click originated from a child element (an ElementRenderer),
90
+ // interact.js has already fired its tap event and called handleSelectElement.
91
+ // We must not clear the selection here in that case.
92
+ if (event.target !== event.currentTarget) return;
93
+ if (this.args.onDeselectAll) {
94
+ this.args.onDeselectAll();
95
+ }
96
+ }
97
+
98
+ // -------------------------------------------------------------------------
99
+ // Helpers
100
+ // -------------------------------------------------------------------------
101
+
102
+ _unitToPx(value, unit) {
103
+ const PPI = 96;
104
+ switch (unit) {
105
+ case 'mm':
106
+ return Math.round((value / 25.4) * PPI);
107
+ case 'cm':
108
+ return Math.round((value / 2.54) * PPI);
109
+ case 'in':
110
+ return Math.round(value * PPI);
111
+ case 'px':
112
+ default:
113
+ return Math.round(value);
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,126 @@
1
+ {{! Template Builder Element Renderer }}
2
+ {{! Each element owns its own interact.js instance. did-insert sets it up;
3
+ will-destroy tears it down. The parent canvas passes @onMove, @onResize,
4
+ and @onSelect callbacks — it never touches the DOM directly. }}
5
+ <div
6
+ class="tb-element {{this.selectionClass}}"
7
+ style={{this.wrapperStyle}}
8
+ data-element-type={{this.elementType}}
9
+ data-element-uuid={{@element.uuid}}
10
+ {{did-insert this.handleInsert}}
11
+ {{did-update this.handleUpdate @element}}
12
+ {{will-destroy this.handleDestroy}}
13
+ >
14
+ {{! TEXT }}
15
+ {{#if this.isText}}
16
+ <div class="tb-element-text" style={{this.textStyle}}>
17
+ {{this.textContent}}
18
+ </div>
19
+ {{/if}}
20
+
21
+ {{! IMAGE }}
22
+ {{#if this.isImage}}
23
+ {{#if this.imageSrc}}
24
+ <img
25
+ src={{this.imageSrc}}
26
+ alt={{this.imageAlt}}
27
+ style={{this.imageStyle}}
28
+ draggable="false"
29
+ />
30
+ {{else}}
31
+ <div class="w-full h-full flex items-center justify-center bg-gray-100 dark:bg-gray-700 border border-dashed border-gray-300 dark:border-gray-600 rounded">
32
+ <div class="text-center text-gray-400 dark:text-gray-500 text-xs">
33
+ <FaIcon @icon="image" @size="lg" class="mb-1 block" />
34
+ <span>Image</span>
35
+ </div>
36
+ </div>
37
+ {{/if}}
38
+ {{/if}}
39
+
40
+ {{! TABLE }}
41
+ {{#if this.isTable}}
42
+ <div class="tb-element-table w-full h-full overflow-auto">
43
+ <table class="w-full border-collapse text-xs" style={{this.tableBorderStyle}}>
44
+ {{#if this.tableColumns}}
45
+ <thead>
46
+ <tr>
47
+ {{#each this.tableColumns as |col|}}
48
+ <th class="border px-2 py-1 text-left" style={{this.tableHeaderStyle}}>
49
+ {{col.label}}
50
+ </th>
51
+ {{/each}}
52
+ </tr>
53
+ </thead>
54
+ <tbody>
55
+ {{#if this.tableRows.length}}
56
+ {{#each this.tableRows as |row|}}
57
+ <tr>
58
+ {{#each this.tableColumns as |col|}}
59
+ <td class="border px-2 py-1" style={{this.tableCellStyle}}>
60
+ {{get row col.key}}
61
+ </td>
62
+ {{/each}}
63
+ </tr>
64
+ {{/each}}
65
+ {{else}}
66
+ {{! Preview placeholder rows }}
67
+ {{#each (array 1 2 3)}}
68
+ <tr>
69
+ {{#each this.tableColumns}}
70
+ <td class="border px-2 py-1 text-gray-400" style={{this.tableCellStyle}}>—</td>
71
+ {{/each}}
72
+ </tr>
73
+ {{/each}}
74
+ {{/if}}
75
+ </tbody>
76
+ {{else}}
77
+ <tbody>
78
+ <tr>
79
+ <td class="border px-2 py-1 text-gray-400 text-center" colspan="1">No columns defined</td>
80
+ </tr>
81
+ </tbody>
82
+ {{/if}}
83
+ </table>
84
+ </div>
85
+ {{/if}}
86
+
87
+ {{! LINE }}
88
+ {{#if this.isLine}}
89
+ <div class="tb-element-line" style={{this.lineStyle}}>
90
+ <div style={{this.lineInnerStyle}}></div>
91
+ </div>
92
+ {{/if}}
93
+
94
+ {{! SHAPE }}
95
+ {{#if this.isShape}}
96
+ <div class="tb-element-shape" style={{this.shapeStyle}}></div>
97
+ {{/if}}
98
+
99
+ {{! QR CODE }}
100
+ {{#if this.isQrCode}}
101
+ <div class="w-full h-full flex items-center justify-center bg-gray-50 dark:bg-gray-800 border border-dashed border-gray-300 dark:border-gray-600 rounded">
102
+ <div class="text-center text-gray-400 dark:text-gray-500 text-xs">
103
+ <FaIcon @icon="qrcode" @size="2x" class="mb-1 block" />
104
+ <span class="block truncate max-w-full px-1">{{this.codeLabel}}</span>
105
+ </div>
106
+ </div>
107
+ {{/if}}
108
+
109
+ {{! BARCODE }}
110
+ {{#if this.isBarcode}}
111
+ <div class="w-full h-full flex items-center justify-center bg-gray-50 dark:bg-gray-800 border border-dashed border-gray-300 dark:border-gray-600 rounded">
112
+ <div class="text-center text-gray-400 dark:text-gray-500 text-xs">
113
+ <FaIcon @icon="barcode" @size="2x" class="mb-1 block" />
114
+ <span class="block truncate max-w-full px-1">{{this.codeLabel}}</span>
115
+ </div>
116
+ </div>
117
+ {{/if}}
118
+
119
+ {{! Selection handles (shown when selected) }}
120
+ {{#if @isSelected}}
121
+ <div class="tb-element-handle tb-handle-nw"></div>
122
+ <div class="tb-element-handle tb-handle-ne"></div>
123
+ <div class="tb-element-handle tb-handle-sw"></div>
124
+ <div class="tb-element-handle tb-handle-se"></div>
125
+ {{/if}}
126
+ </div>