@octabits-io/nuxt-ui-kit 0.17.0 → 0.17.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.
- package/dist/index.d.ts +19 -3
- package/dist/index.js +31 -2
- package/package.json +3 -1
- package/src/components/PageActions.vue +16 -1
- package/src/components/PageHeader.vue +12 -2
- package/src/components/SubSidebar.vue +6 -1
package/dist/index.d.ts
CHANGED
|
@@ -242,9 +242,19 @@ interface HelpPanel {
|
|
|
242
242
|
currentActions: ComputedRef<HelpPanelAction[]>;
|
|
243
243
|
/** Whether the active tab has any help actions */
|
|
244
244
|
hasActions: ComputedRef<boolean>;
|
|
245
|
-
/**
|
|
246
|
-
|
|
247
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Register help actions for a tab. Returns a disposer that removes *this*
|
|
247
|
+
* registration and no other — safe to call after the tab has been claimed by
|
|
248
|
+
* a later component, where it is a no-op.
|
|
249
|
+
*/
|
|
250
|
+
register(tabValue: string, actions: HelpPanelAction[]): () => void;
|
|
251
|
+
/**
|
|
252
|
+
* Unregister help actions for a tab.
|
|
253
|
+
*
|
|
254
|
+
* Only the component that registered the tab can remove it (see the
|
|
255
|
+
* ownership note on `useHelpPanel`). Registrations made outside a component
|
|
256
|
+
* have no owner and are removed unconditionally.
|
|
257
|
+
*/
|
|
248
258
|
unregister(tabValue: string): void;
|
|
249
259
|
/** Set the currently active tab */
|
|
250
260
|
setActiveTab(tabValue: string): void;
|
|
@@ -265,6 +275,12 @@ interface HelpPanelOptions {
|
|
|
265
275
|
* localStorage; switching to a tab without actions auto-closes the panel.
|
|
266
276
|
*
|
|
267
277
|
* Provide it per page: `provide(HELP_PANEL_KEY, useHelpPanel())`.
|
|
278
|
+
*
|
|
279
|
+
* **A registration belongs to the component that made it.** Where one registry
|
|
280
|
+
* is shared across routes, two components routinely share a tab value, and
|
|
281
|
+
* teardown of the old one interleaves with setup of the new — so removal is
|
|
282
|
+
* owner-checked rather than by key alone. Prefer the disposer `register`
|
|
283
|
+
* returns; `unregister(tab)` is equivalent and stays for existing callers.
|
|
268
284
|
*/
|
|
269
285
|
declare function useHelpPanel(options?: HelpPanelOptions): HelpPanel;
|
|
270
286
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, reactive, ref, watch } from "vue";
|
|
1
|
+
import { computed, getCurrentInstance, reactive, ref, watch } from "vue";
|
|
2
2
|
//#region src/org/orgStore.ts
|
|
3
3
|
/**
|
|
4
4
|
* Reactive granted-organizations state + switching — the setup body of an
|
|
@@ -177,6 +177,12 @@ const HELP_PANEL_KEY = Symbol("help-panel");
|
|
|
177
177
|
* localStorage; switching to a tab without actions auto-closes the panel.
|
|
178
178
|
*
|
|
179
179
|
* Provide it per page: `provide(HELP_PANEL_KEY, useHelpPanel())`.
|
|
180
|
+
*
|
|
181
|
+
* **A registration belongs to the component that made it.** Where one registry
|
|
182
|
+
* is shared across routes, two components routinely share a tab value, and
|
|
183
|
+
* teardown of the old one interleaves with setup of the new — so removal is
|
|
184
|
+
* owner-checked rather than by key alone. Prefer the disposer `register`
|
|
185
|
+
* returns; `unregister(tab)` is equivalent and stays for existing callers.
|
|
180
186
|
*/
|
|
181
187
|
function useHelpPanel(options = {}) {
|
|
182
188
|
const storageKey = options.storageKey ?? "help-panel-open";
|
|
@@ -189,11 +195,34 @@ function useHelpPanel(options = {}) {
|
|
|
189
195
|
return registrations.get(activeTabValue.value)?.actions ?? [];
|
|
190
196
|
});
|
|
191
197
|
const hasActions = computed(() => currentActions.value.length > 0);
|
|
198
|
+
/**
|
|
199
|
+
* Which component owns each tab's registration.
|
|
200
|
+
*
|
|
201
|
+
* Consumers key registrations by *surface*, so several pages legitimately
|
|
202
|
+
* share one tab value (an admin console where every flat page registers
|
|
203
|
+
* `'detail'` is the motivating case). On a client-side navigation Vue runs
|
|
204
|
+
* the INCOMING component's `setup()` before the outgoing one's
|
|
205
|
+
* `onUnmounted`, so the calls arrive as: new registers, old unregisters. A
|
|
206
|
+
* delete-by-key therefore let a departing component remove its successor's
|
|
207
|
+
* registration, and the help trigger vanished for the rest of the session —
|
|
208
|
+
* every arrival wiped by the page it had just replaced.
|
|
209
|
+
*/
|
|
210
|
+
const owners = /* @__PURE__ */ new Map();
|
|
211
|
+
function remove(tabValue, caller) {
|
|
212
|
+
if (owners.has(tabValue) && owners.get(tabValue) !== caller) return;
|
|
213
|
+
owners.delete(tabValue);
|
|
214
|
+
registrations.delete(tabValue);
|
|
215
|
+
}
|
|
192
216
|
function register(tabValue, actions) {
|
|
217
|
+
const owner = getCurrentInstance();
|
|
218
|
+
if (owner) owners.set(tabValue, owner);
|
|
193
219
|
registrations.set(tabValue, { actions });
|
|
220
|
+
return () => {
|
|
221
|
+
remove(tabValue, owner);
|
|
222
|
+
};
|
|
194
223
|
}
|
|
195
224
|
function unregister(tabValue) {
|
|
196
|
-
|
|
225
|
+
remove(tabValue, getCurrentInstance());
|
|
197
226
|
}
|
|
198
227
|
function setActiveTab(tabValue) {
|
|
199
228
|
activeTabValue.value = tabValue;
|
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.2",
|
|
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",
|
|
@@ -73,7 +73,9 @@
|
|
|
73
73
|
"url": "https://github.com/octabits-io/platform/issues"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
+
"@vue/test-utils": "^2.4.6",
|
|
76
77
|
"date-fns": "^4.4.0",
|
|
78
|
+
"happy-dom": "^15.11.7",
|
|
77
79
|
"oidc-client-ts": "^3.5.0",
|
|
78
80
|
"vitest": "^4.1.11",
|
|
79
81
|
"vue": "^3.5.41",
|
|
@@ -177,6 +177,21 @@ const menuGroups = computed<DropdownMenuItem[][]>(() => {
|
|
|
177
177
|
const hasUtilityRegion = computed(() =>
|
|
178
178
|
inlineUtilityItems.value.length > 0 || (showHelp.value && !utilitiesCollapsed.value),
|
|
179
179
|
);
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Is there anything to the LEFT of the utility region for the separator to
|
|
183
|
+
* separate it from? Everything the template renders before it, in order:
|
|
184
|
+
* the inline actions, the AI cluster, and the overflow menu (which draws
|
|
185
|
+
* nothing when its group list is empty).
|
|
186
|
+
*
|
|
187
|
+
* Without this the rule is drawn against the start of the cluster. A record
|
|
188
|
+
* route that declares no actions — only the help registry — renders a bar
|
|
189
|
+
* whose entire content is a vertical line and the Help button beside it, the
|
|
190
|
+
* line dividing Help from nothing.
|
|
191
|
+
*/
|
|
192
|
+
const hasLeadingContent = computed(() =>
|
|
193
|
+
inlineEntries.value.length > 0 || inlineAiItems.value.length > 0 || menuGroups.value.length > 0,
|
|
194
|
+
);
|
|
180
195
|
</script>
|
|
181
196
|
|
|
182
197
|
<template>
|
|
@@ -251,7 +266,7 @@ const hasUtilityRegion = computed(() =>
|
|
|
251
266
|
</UDropdownMenu>
|
|
252
267
|
<PageActionMenu :items="menuGroups" />
|
|
253
268
|
<template v-if="hasUtilityRegion">
|
|
254
|
-
<USeparator orientation="vertical" class="h-5 mx-1" />
|
|
269
|
+
<USeparator v-if="hasLeadingContent" orientation="vertical" class="h-5 mx-1" />
|
|
255
270
|
<PageAction
|
|
256
271
|
v-for="item in inlineUtilityItems"
|
|
257
272
|
:key="item.key"
|
|
@@ -117,11 +117,21 @@ const titleClass = computed(() => props.density === 'compact' ? 'font-display te
|
|
|
117
117
|
* that was stacking.
|
|
118
118
|
*/
|
|
119
119
|
const inlineHeading = computed(() => props.density === 'compact' && !slots.title)
|
|
120
|
+
/**
|
|
121
|
+
* No wrapping, and a subtitle that shrinks before anything else does.
|
|
122
|
+
*
|
|
123
|
+
* The wrapper is `flex-wrap`, so without `min-w-0` + `truncate` a long
|
|
124
|
+
* subtitle keeps its min-content width, wins the line, and pushes the action
|
|
125
|
+
* cluster onto a second row — a compact band that ends up TALLER than the
|
|
126
|
+
* stacked one it replaced (observed on the places page: 101px against 85).
|
|
127
|
+
* Truncating is the right degradation here: `compact` promises one row, and a
|
|
128
|
+
* subtitle long enough to truncate is help-panel material, not chrome.
|
|
129
|
+
*/
|
|
120
130
|
const headingClass = computed(() => inlineHeading.value
|
|
121
|
-
? 'flex min-w-0
|
|
131
|
+
? 'flex min-w-0 items-baseline gap-x-2'
|
|
122
132
|
: 'min-w-0')
|
|
123
133
|
const subtitleClass = computed(() => inlineHeading.value
|
|
124
|
-
? 'text-sm text-muted min-w-0'
|
|
134
|
+
? 'text-sm text-muted min-w-0 truncate'
|
|
125
135
|
: 'text-sm text-muted mt-1')
|
|
126
136
|
</script>
|
|
127
137
|
|
|
@@ -66,8 +66,13 @@ watch(
|
|
|
66
66
|
row down, on fixed chrome that never scrolls away). That is a visual
|
|
67
67
|
economy, not a semantic one: the page still has a name, and with nothing
|
|
68
68
|
selected the detail column has no heading of its own to stand in for it.
|
|
69
|
+
|
|
70
|
+
`headerless` only: without it the rail draws its own visible heading, and
|
|
71
|
+
a second, invisible one for the same shell is noise. The settings layout
|
|
72
|
+
nests one of each, which is how that showed up — "Settings" announced
|
|
73
|
+
twice before "Places".
|
|
69
74
|
-->
|
|
70
|
-
<h1 class="sr-only">{{ title }}</h1>
|
|
75
|
+
<h1 v-if="headerless" class="sr-only">{{ title }}</h1>
|
|
71
76
|
|
|
72
77
|
<!-- Page header zone: rendered once (unlike #sidebar), full width above both columns -->
|
|
73
78
|
<div v-if="$slots.header" class="shrink-0">
|