@fleetbase/ember-ui 0.3.22 → 0.3.23

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.
@@ -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() {
@@ -169,13 +169,22 @@
169
169
 
170
170
  /* ── Extension card ─────────────────────────────────────────────────────────── */
171
171
 
172
+ /* Wrapper that holds the card link + absolutely-positioned pin button */
173
+ .snm-dropdown-card-wrap {
174
+ position: relative;
175
+ }
176
+
172
177
  .snm-dropdown-card {
173
178
  @apply flex flex-col rounded-lg;
174
179
  padding: 7px 10px;
175
180
  background-color: #111827; /* gray-900 */
176
181
  border: 1px solid #1f2937; /* gray-800 – darker but lighter than bg */
177
182
  transition: border-color 0.15s ease, background-color 0.15s ease;
178
- position: relative;
183
+ text-decoration: none;
184
+ color: inherit;
185
+ display: flex;
186
+ width: 100%;
187
+ box-sizing: border-box;
179
188
  }
180
189
 
181
190
  .snm-dropdown-card:hover {
@@ -183,36 +192,29 @@
183
192
  background-color: #1a2332; /* slightly lighter than gray-900 */
184
193
  }
185
194
 
186
- /* Card header row: icon + title link + pin button */
195
+ /* Card header row: icon + title + pin button */
187
196
  .snm-dropdown-card-header {
188
197
  @apply flex items-center;
189
198
  gap: 0;
190
199
  margin-bottom: 0;
191
200
  }
192
201
 
193
- /* The main extension link inside a card */
194
- .snm-dropdown-card-link {
195
- @apply flex items-center flex-1 text-sm font-medium;
196
- color: #e5e7eb; /* gray-200 */
197
- text-decoration: none;
198
- min-width: 0;
199
- cursor: pointer;
200
- border: none;
201
- background: none;
202
- padding: 0;
203
- }
204
-
205
- .snm-dropdown-card-link:hover {
206
- color: #f9fafb; /* gray-50 */
207
- }
208
-
209
- .snm-dropdown-card-link-wrapper {
210
- @apply flex flex-1;
211
- min-width: 0;
202
+ /* Pin button is a sibling of the card link, absolutely positioned top-right
203
+ so it floats above the card and receives its own clicks */
204
+ .snm-dropdown-pin-btn {
205
+ position: absolute;
206
+ top: 6px;
207
+ right: 6px;
208
+ z-index: 1;
212
209
  }
213
210
 
214
- .snm-dropdown-card-link-wrapper .snm-dropdown-card-link {
215
- width: 100%;
211
+ /* Card-as-link: the card itself is the <LinkToExternal> or <a> */
212
+ a.snm-dropdown-card,
213
+ .snm-dropdown-card[href] {
214
+ display: flex;
215
+ text-decoration: none;
216
+ color: inherit;
217
+ cursor: pointer;
216
218
  }
217
219
 
218
220
  .snm-dropdown-card-icon {
@@ -250,12 +252,13 @@
250
252
  }
251
253
 
252
254
  /* Pin button on card – always visible at low opacity, brightens on hover */
253
- .snm-dropdown-card .snm-dropdown-pin-btn {
254
- opacity: 0.35;
255
+ .snm-dropdown-card-wrap .snm-dropdown-pin-btn {
256
+ opacity: 0;
255
257
  pointer-events: auto;
258
+ transition: opacity 0.15s ease;
256
259
  }
257
260
 
258
- .snm-dropdown-card:hover .snm-dropdown-pin-btn {
261
+ .snm-dropdown-card-wrap:hover .snm-dropdown-pin-btn {
259
262
  opacity: 1;
260
263
  }
261
264
 
@@ -787,11 +790,13 @@ body[data-theme='light'] .snm-dropdown-card:hover {
787
790
  background-color: #f3f4f6; /* gray-100 */
788
791
  }
789
792
 
790
- body[data-theme='light'] .snm-dropdown-card-link {
793
+ body[data-theme='light'] a.snm-dropdown-card,
794
+ body[data-theme='light'] .snm-dropdown-card[href] {
791
795
  color: #374151; /* gray-700 */
792
796
  }
793
797
 
794
- body[data-theme='light'] .snm-dropdown-card-link:hover {
798
+ body[data-theme='light'] a.snm-dropdown-card:hover,
799
+ body[data-theme='light'] .snm-dropdown-card[href]:hover {
795
800
  color: #111827; /* gray-900 */
796
801
  }
797
802
 
@@ -799,7 +804,8 @@ body[data-theme='light'] .snm-dropdown-card-icon {
799
804
  color: #6b7280; /* gray-500 */
800
805
  }
801
806
 
802
- body[data-theme='light'] .snm-dropdown-card-link:hover .snm-dropdown-card-icon {
807
+ body[data-theme='light'] a.snm-dropdown-card:hover .snm-dropdown-card-icon,
808
+ body[data-theme='light'] .snm-dropdown-card[href]:hover .snm-dropdown-card-icon {
803
809
  color: #2563eb; /* blue-600 */
804
810
  }
805
811
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/ember-ui",
3
- "version": "0.3.22",
3
+ "version": "0.3.23",
4
4
  "description": "Fleetbase UI provides all the interface components, helpers, services and utilities for building a Fleetbase extension into the Console.",
5
5
  "keywords": [
6
6
  "fleetbase-ui",