@octabits-io/nuxt-ui-kit 0.17.1 → 0.17.3
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octabits-io/nuxt-ui-kit",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
4
4
|
"description": "Frontend kit for Nuxt/Vue admin SPAs: OIDC session harness (oidc-client-ts), API-client seams (base URL + OIDC bearer), auth/org store cores, and a route-guard builder — factory-style seams the app wires into its own plugins, stores, and middleware",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
foldInlineActions,
|
|
22
22
|
groupIsPrimary,
|
|
23
23
|
isInlineBound as isItemInlineBound,
|
|
24
|
+
buildMenuActionGroups,
|
|
24
25
|
resolveCollapseStages,
|
|
25
26
|
type PageActionsItem,
|
|
26
27
|
} from './pageActions.ts';
|
|
@@ -137,26 +138,6 @@ function toMenuItem(item: PageActionsItem): DropdownMenuItem {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
const menuGroups = computed<DropdownMenuItem[][]>(() => {
|
|
140
|
-
const collapsedAutos = collapsed.value
|
|
141
|
-
? actionItems.value.filter(item => (item.visibility ?? 'auto') === 'auto')
|
|
142
|
-
: [];
|
|
143
|
-
|
|
144
|
-
// Menu-only items grouped by section, in first-appearance order.
|
|
145
|
-
const sections = new Map<string, PageActionsItem[]>();
|
|
146
|
-
for (const item of actionItems.value) {
|
|
147
|
-
if ((item.visibility ?? 'auto') !== 'menu') continue;
|
|
148
|
-
const section = item.section ?? item.group?.id ?? 'default';
|
|
149
|
-
if (!sections.has(section)) sections.set(section, []);
|
|
150
|
-
sections.get(section)!.push(item);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// AI items bound to the menu (explicit 'menu', or 'auto' while collapsed)
|
|
154
|
-
// form their own group between the action sections and the utilities.
|
|
155
|
-
const aiGroup = aiItems.value.filter(item =>
|
|
156
|
-
(item.visibility ?? 'auto') === 'menu'
|
|
157
|
-
|| ((item.visibility ?? 'auto') === 'auto' && collapsed.value),
|
|
158
|
-
);
|
|
159
|
-
|
|
160
141
|
const utilityGroup: DropdownMenuItem[] = utilitiesCollapsed.value
|
|
161
142
|
? [
|
|
162
143
|
...props.utilityItems.map(toMenuItem),
|
|
@@ -166,10 +147,11 @@ const menuGroups = computed<DropdownMenuItem[][]>(() => {
|
|
|
166
147
|
]
|
|
167
148
|
: [];
|
|
168
149
|
|
|
150
|
+
// Action groups (incl. their ORDER — see buildMenuActionGroups) are pure and
|
|
151
|
+
// live in the module; the utilities need i18n and the help registry.
|
|
169
152
|
return [
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
aiGroup.map(toMenuItem),
|
|
153
|
+
...buildMenuActionGroups(actionItems.value, aiItems.value, collapsed.value)
|
|
154
|
+
.map(group => group.map(toMenuItem)),
|
|
173
155
|
utilityGroup,
|
|
174
156
|
].filter(group => group.length > 0);
|
|
175
157
|
});
|
|
@@ -177,6 +159,21 @@ const menuGroups = computed<DropdownMenuItem[][]>(() => {
|
|
|
177
159
|
const hasUtilityRegion = computed(() =>
|
|
178
160
|
inlineUtilityItems.value.length > 0 || (showHelp.value && !utilitiesCollapsed.value),
|
|
179
161
|
);
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Is there anything to the LEFT of the utility region for the separator to
|
|
165
|
+
* separate it from? Everything the template renders before it, in order:
|
|
166
|
+
* the inline actions, the AI cluster, and the overflow menu (which draws
|
|
167
|
+
* nothing when its group list is empty).
|
|
168
|
+
*
|
|
169
|
+
* Without this the rule is drawn against the start of the cluster. A record
|
|
170
|
+
* route that declares no actions — only the help registry — renders a bar
|
|
171
|
+
* whose entire content is a vertical line and the Help button beside it, the
|
|
172
|
+
* line dividing Help from nothing.
|
|
173
|
+
*/
|
|
174
|
+
const hasLeadingContent = computed(() =>
|
|
175
|
+
inlineEntries.value.length > 0 || inlineAiItems.value.length > 0 || menuGroups.value.length > 0,
|
|
176
|
+
);
|
|
180
177
|
</script>
|
|
181
178
|
|
|
182
179
|
<template>
|
|
@@ -251,7 +248,7 @@ const hasUtilityRegion = computed(() =>
|
|
|
251
248
|
</UDropdownMenu>
|
|
252
249
|
<PageActionMenu :items="menuGroups" />
|
|
253
250
|
<template v-if="hasUtilityRegion">
|
|
254
|
-
<USeparator orientation="vertical" class="h-5 mx-1" />
|
|
251
|
+
<USeparator v-if="hasLeadingContent" orientation="vertical" class="h-5 mx-1" />
|
|
255
252
|
<PageAction
|
|
256
253
|
v-for="item in inlineUtilityItems"
|
|
257
254
|
:key="item.key"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
|
+
buildMenuActionGroups,
|
|
3
4
|
foldInlineActions,
|
|
4
5
|
groupIsPrimary,
|
|
5
6
|
isInlineBound,
|
|
@@ -156,3 +157,53 @@ describe('resolveCollapseStages', () => {
|
|
|
156
157
|
}
|
|
157
158
|
});
|
|
158
159
|
});
|
|
160
|
+
|
|
161
|
+
describe('buildMenuActionGroups', () => {
|
|
162
|
+
const item = (over: Partial<PageActionsItem>): PageActionsItem =>
|
|
163
|
+
({ key: over.key ?? 'k', icon: 'i', label: over.label ?? 'l', ...over });
|
|
164
|
+
|
|
165
|
+
const keys = (groups: PageActionsItem[][]) => groups.map(g => g.map(i => i.key));
|
|
166
|
+
|
|
167
|
+
it('keeps an AI row above the destructive section', () => {
|
|
168
|
+
// The regression this exists for: an AI item appended AFTER the sections
|
|
169
|
+
// rendered under "Delete", where a "Generate page content" row reads as an
|
|
170
|
+
// afterthought to the deletion.
|
|
171
|
+
const actions = [
|
|
172
|
+
item({ key: 'export', visibility: 'menu' }),
|
|
173
|
+
item({ key: 'delete', visibility: 'menu', section: 'destructive', color: 'error' }),
|
|
174
|
+
];
|
|
175
|
+
const ai = [item({ key: 'generate', kind: 'ai' })];
|
|
176
|
+
|
|
177
|
+
expect(keys(buildMenuActionGroups(actions, ai, true)))
|
|
178
|
+
.toEqual([['generate'], ['export'], ['delete']]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('sits with the other collapsed actions, not behind them', () => {
|
|
182
|
+
const actions = [item({ key: 'edit' }), item({ key: 'delete', visibility: 'menu' })];
|
|
183
|
+
const ai = [item({ key: 'generate', kind: 'ai' })];
|
|
184
|
+
|
|
185
|
+
// 'edit' collapses out of the bar; the AI row is its own group beside it.
|
|
186
|
+
expect(keys(buildMenuActionGroups(actions, ai, true)))
|
|
187
|
+
.toEqual([['edit'], ['generate'], ['delete']]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('leaves inline-bound items out of the menu entirely', () => {
|
|
191
|
+
const actions = [item({ key: 'publish', visibility: 'always' }), item({ key: 'edit' })];
|
|
192
|
+
const ai = [item({ key: 'generate', kind: 'ai', visibility: 'always' })];
|
|
193
|
+
|
|
194
|
+
// Wide: nothing collapses. Narrow: 'always' items still render inline, so
|
|
195
|
+
// the menu holds only 'edit'.
|
|
196
|
+
expect(keys(buildMenuActionGroups(actions, ai, false))).toEqual([]);
|
|
197
|
+
expect(keys(buildMenuActionGroups(actions, ai, true))).toEqual([['edit']]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('orders menu sections by first appearance, so the caller decides', () => {
|
|
201
|
+
const actions = [
|
|
202
|
+
item({ key: 'a', visibility: 'menu', section: 'one' }),
|
|
203
|
+
item({ key: 'b', visibility: 'menu', section: 'two' }),
|
|
204
|
+
item({ key: 'c', visibility: 'menu', section: 'one' }),
|
|
205
|
+
];
|
|
206
|
+
expect(keys(buildMenuActionGroups(actions, [], false)))
|
|
207
|
+
.toEqual([['a', 'c'], ['b']]);
|
|
208
|
+
});
|
|
209
|
+
})
|
|
@@ -193,3 +193,46 @@ export function resolveCollapseStages(
|
|
|
193
193
|
utilitiesCollapsed: collapsed || width < (utilityCollapseBelow ?? collapseBelow),
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The overflow menu's ACTION groups, in render order — everything above the
|
|
199
|
+
* utility group, which the component builds itself (it needs i18n and the help
|
|
200
|
+
* registry).
|
|
201
|
+
*
|
|
202
|
+
* The order is the whole point, and it is a convention the type cannot express:
|
|
203
|
+
* destructive rows are the last-declared menu section, so anything appended
|
|
204
|
+
* after the sections lands under "Delete". AI items used to, which read as an
|
|
205
|
+
* afterthought to the deletion rather than as a thing you might do to the
|
|
206
|
+
* record. They belong with the other collapsed actions instead.
|
|
207
|
+
*
|
|
208
|
+
* Groups render separated, and empty ones are dropped by the caller.
|
|
209
|
+
*/
|
|
210
|
+
export function buildMenuActionGroups(
|
|
211
|
+
actionItems: PageActionsItem[],
|
|
212
|
+
aiItems: PageActionsItem[],
|
|
213
|
+
collapsed: boolean,
|
|
214
|
+
): PageActionsItem[][] {
|
|
215
|
+
const visibilityOf = (item: PageActionsItem) => item.visibility ?? 'auto';
|
|
216
|
+
|
|
217
|
+
// 'auto' actions the current width pushed out of the bar.
|
|
218
|
+
const collapsedAutos = collapsed
|
|
219
|
+
? actionItems.filter(item => visibilityOf(item) === 'auto')
|
|
220
|
+
: [];
|
|
221
|
+
|
|
222
|
+
// Menu-only items, grouped by section in first-appearance order — so a
|
|
223
|
+
// caller's declaration order decides, and 'destructive' stays last by
|
|
224
|
+
// being declared last.
|
|
225
|
+
const sections = new Map<string, PageActionsItem[]>();
|
|
226
|
+
for (const item of actionItems) {
|
|
227
|
+
if (visibilityOf(item) !== 'menu') continue;
|
|
228
|
+
const section = item.section ?? item.group?.id ?? 'default';
|
|
229
|
+
if (!sections.has(section)) sections.set(section, []);
|
|
230
|
+
sections.get(section)!.push(item);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const aiGroup = aiItems.filter(item =>
|
|
234
|
+
visibilityOf(item) === 'menu' || (visibilityOf(item) === 'auto' && collapsed),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return [collapsedAutos, aiGroup, ...sections.values()].filter(group => group.length > 0);
|
|
238
|
+
}
|