@octabits-io/nuxt-ui-kit 0.9.1 → 0.11.0
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 +48 -1
- package/dist/index.js +10 -1
- package/package.json +1 -1
- package/src/components/PageActions.vue +151 -0
- package/src/components/PageHeader.vue +24 -2
- package/src/components/pageActions.ts +49 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Component, ComputedRef, InjectionKey, Ref } from "vue";
|
|
2
|
+
import { RouteLocationRaw } from "vue-router";
|
|
2
3
|
//#region src/org/orgStore.d.ts
|
|
3
4
|
type OrgStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
|
|
4
5
|
/** Result seam for the org fetch — mirrors an Eden Treaty `{ data, error }`. */
|
|
@@ -117,6 +118,52 @@ declare function createApiErrorMessenger(options: ApiErrorMessengerOptions): {
|
|
|
117
118
|
isValidationError: (error: unknown) => error is ValidationApiErrorLike;
|
|
118
119
|
};
|
|
119
120
|
//#endregion
|
|
121
|
+
//#region src/components/pageActions.d.ts
|
|
122
|
+
/**
|
|
123
|
+
* Declarative page-header action. One array describes every action of a page —
|
|
124
|
+
* inline buttons and overflow-menu items alike — and `PageActions` decides
|
|
125
|
+
* placement from `visibility` and the available header width.
|
|
126
|
+
*/
|
|
127
|
+
interface PageActionsItem {
|
|
128
|
+
/** Stable identity (also the Vue key). */
|
|
129
|
+
key: string;
|
|
130
|
+
icon: string;
|
|
131
|
+
label: string;
|
|
132
|
+
/** Inline button tone. At most ONE 'primary' item should be visible per state. */
|
|
133
|
+
tone?: 'primary' | 'neutral';
|
|
134
|
+
/**
|
|
135
|
+
* Placement tier:
|
|
136
|
+
* - 'always' — inline at every width (the state's main action)
|
|
137
|
+
* - 'auto' — inline when the header is wide enough, else moved into the
|
|
138
|
+
* overflow menu (default)
|
|
139
|
+
* - 'menu' — overflow-menu only
|
|
140
|
+
*/
|
|
141
|
+
visibility?: 'always' | 'auto' | 'menu';
|
|
142
|
+
/**
|
|
143
|
+
* Menu group id for 'menu' items (default 'default'). Groups render as
|
|
144
|
+
* separated menu sections in first-appearance order; collapsed 'auto' items
|
|
145
|
+
* form the leading group and utility items the trailing one. Convention:
|
|
146
|
+
* put destructive rows in the last-declared section.
|
|
147
|
+
*/
|
|
148
|
+
section?: string;
|
|
149
|
+
/** Menu-item color (e.g. 'error' for destructive rows). Inline tone wins inline. */
|
|
150
|
+
color?: 'error' | 'warning';
|
|
151
|
+
loading?: boolean;
|
|
152
|
+
disabled?: boolean;
|
|
153
|
+
/** Blocked-with-reason: renders disabled; inline buttons tooltip "label — reason". */
|
|
154
|
+
disabledReason?: string | null;
|
|
155
|
+
to?: RouteLocationRaw;
|
|
156
|
+
target?: string;
|
|
157
|
+
onSelect?: () => void;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Current PageHeader content width in px, provided by `PageHeader` via a
|
|
161
|
+
* ResizeObserver. `null` until first measurement (treated as wide).
|
|
162
|
+
*/
|
|
163
|
+
declare const PAGE_HEADER_WIDTH: InjectionKey<Ref<number | null>>;
|
|
164
|
+
/** Below this header width (px), 'auto' actions and utilities collapse into the menu. */
|
|
165
|
+
declare const PAGE_ACTIONS_COLLAPSE_BELOW = 640;
|
|
166
|
+
//#endregion
|
|
120
167
|
//#region src/composables/useHelpPanel.d.ts
|
|
121
168
|
interface HelpPanelAction {
|
|
122
169
|
/** Unique key for this action within the tab */
|
|
@@ -205,4 +252,4 @@ declare function usePagination(options?: {
|
|
|
205
252
|
resetPagination: () => void;
|
|
206
253
|
};
|
|
207
254
|
//#endregion
|
|
208
|
-
export { type ApiErrorLike, type ApiErrorMessengerOptions, type ConfirmOptions, type FetchOrganizationsResult, HELP_PANEL_KEY, type HelpPanel, type HelpPanelAction, type HelpPanelOptions, type HelpPanelRegistration, type OrgStoreCore, type OrgStoreCoreOptions, type ValidationApiErrorLike, createApiErrorMessenger, createOrgStoreCore, resolveRuntimeConfigValue, useConfirm, useConfirmState, useDirtyTracking, useHelpPanel, usePagination };
|
|
255
|
+
export { type ApiErrorLike, type ApiErrorMessengerOptions, type ConfirmOptions, type FetchOrganizationsResult, HELP_PANEL_KEY, type HelpPanel, type HelpPanelAction, type HelpPanelOptions, type HelpPanelRegistration, type OrgStoreCore, type OrgStoreCoreOptions, PAGE_ACTIONS_COLLAPSE_BELOW, PAGE_HEADER_WIDTH, type PageActionsItem, type ValidationApiErrorLike, createApiErrorMessenger, createOrgStoreCore, resolveRuntimeConfigValue, useConfirm, useConfirmState, useDirtyTracking, useHelpPanel, usePagination };
|
package/dist/index.js
CHANGED
|
@@ -156,6 +156,15 @@ function createApiErrorMessenger(options) {
|
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
158
|
//#endregion
|
|
159
|
+
//#region src/components/pageActions.ts
|
|
160
|
+
/**
|
|
161
|
+
* Current PageHeader content width in px, provided by `PageHeader` via a
|
|
162
|
+
* ResizeObserver. `null` until first measurement (treated as wide).
|
|
163
|
+
*/
|
|
164
|
+
const PAGE_HEADER_WIDTH = Symbol("page-header-width");
|
|
165
|
+
/** Below this header width (px), 'auto' actions and utilities collapse into the menu. */
|
|
166
|
+
const PAGE_ACTIONS_COLLAPSE_BELOW = 640;
|
|
167
|
+
//#endregion
|
|
159
168
|
//#region src/composables/useHelpPanel.ts
|
|
160
169
|
const HELP_PANEL_KEY = Symbol("help-panel");
|
|
161
170
|
/**
|
|
@@ -268,4 +277,4 @@ function usePagination(options = {}) {
|
|
|
268
277
|
};
|
|
269
278
|
}
|
|
270
279
|
//#endregion
|
|
271
|
-
export { HELP_PANEL_KEY, createApiErrorMessenger, createOrgStoreCore, resolveRuntimeConfigValue, useConfirm, useConfirmState, useDirtyTracking, useHelpPanel, usePagination };
|
|
280
|
+
export { HELP_PANEL_KEY, PAGE_ACTIONS_COLLAPSE_BELOW, PAGE_HEADER_WIDTH, createApiErrorMessenger, createOrgStoreCore, resolveRuntimeConfigValue, useConfirm, useConfirmState, useDirtyTracking, useHelpPanel, usePagination };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octabits-io/nuxt-ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Frontend kit for Nuxt/Vue admin SPAs: OIDC session harness (oidc-client-ts), Eden Treaty client factory, 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",
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shipped as source: the consumer's Vite compiles this SFC. All imports are
|
|
3
|
+
// explicit — no reliance on the consumer's auto-import configuration.
|
|
4
|
+
// i18n key contract: pageChrome.help (+ PageActionMenu's pageChrome.moreActions).
|
|
5
|
+
import { computed, inject } from 'vue';
|
|
6
|
+
import { useI18n } from 'vue-i18n';
|
|
7
|
+
import USeparator from '@nuxt/ui/components/Separator.vue';
|
|
8
|
+
import type { DropdownMenuItem } from '@nuxt/ui';
|
|
9
|
+
import PageAction from './PageAction.vue';
|
|
10
|
+
import PageActionMenu from './PageActionMenu.vue';
|
|
11
|
+
// Package-name import (not ../composables): only src/components is packed, and
|
|
12
|
+
// the dist-barrel Symbol instance must match the consumer's HELP_PANEL_KEY provider.
|
|
13
|
+
import { HELP_PANEL_KEY } from '@octabits-io/nuxt-ui-kit';
|
|
14
|
+
import { PAGE_ACTIONS_COLLAPSE_BELOW, PAGE_HEADER_WIDTH, type PageActionsItem } from './pageActions.ts';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Width-aware page-header action cluster: one declarative list drives both the
|
|
18
|
+
* inline buttons and the overflow menu. Wide headers render 'always' + 'auto'
|
|
19
|
+
* items inline (all labeled, one solid primary max); below `collapseBelow`
|
|
20
|
+
* only 'always' items stay inline and everything else — 'auto' items, utility
|
|
21
|
+
* items, and the Help trigger — moves into the ⋯ menu, keeping its label.
|
|
22
|
+
*
|
|
23
|
+
* The Help trigger is rendered automatically when a `HELP_PANEL_KEY` registry
|
|
24
|
+
* with registered actions is provided (replaces `PageUtilityActions` on pages
|
|
25
|
+
* using this component — pass `:utility="false"` to `PageHeader`).
|
|
26
|
+
*/
|
|
27
|
+
const props = withDefaults(defineProps<{
|
|
28
|
+
/** Entity actions (inline and/or menu, per `visibility`). */
|
|
29
|
+
items: PageActionsItem[]
|
|
30
|
+
/** Page-level utility triggers (e.g. "Ask about this booking"). Inline right
|
|
31
|
+
* of a separator when wide; bottom menu group when collapsed. */
|
|
32
|
+
utilityItems?: PageActionsItem[]
|
|
33
|
+
/** Header width (px) below which 'auto'/utility items collapse into the menu. */
|
|
34
|
+
collapseBelow?: number
|
|
35
|
+
/** Render the built-in Help trigger (when a help registry with actions is
|
|
36
|
+
* provided). Disable in nested/panel headers where the page-level header
|
|
37
|
+
* already owns Help. */
|
|
38
|
+
help?: boolean
|
|
39
|
+
}>(), {
|
|
40
|
+
utilityItems: () => [],
|
|
41
|
+
collapseBelow: PAGE_ACTIONS_COLLAPSE_BELOW,
|
|
42
|
+
help: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const { t } = useI18n();
|
|
46
|
+
const helpPanel = inject(HELP_PANEL_KEY, null);
|
|
47
|
+
const headerWidth = inject(PAGE_HEADER_WIDTH, null);
|
|
48
|
+
|
|
49
|
+
// null (no PageHeader provider / pre-measurement) counts as wide — the
|
|
50
|
+
// flex-wrap fallback keeps an unexpectedly narrow first frame usable.
|
|
51
|
+
const collapsed = computed(() => {
|
|
52
|
+
const width = headerWidth?.value;
|
|
53
|
+
return width != null && width < props.collapseBelow;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const showHelp = computed(() => props.help && Boolean(helpPanel?.hasActions.value));
|
|
57
|
+
|
|
58
|
+
const inlineItems = computed(() => props.items.filter(item =>
|
|
59
|
+
(item.visibility ?? 'auto') === 'always'
|
|
60
|
+
|| ((item.visibility ?? 'auto') === 'auto' && !collapsed.value),
|
|
61
|
+
));
|
|
62
|
+
|
|
63
|
+
const inlineUtilityItems = computed(() => collapsed.value ? [] : props.utilityItems);
|
|
64
|
+
|
|
65
|
+
function toMenuItem(item: PageActionsItem): DropdownMenuItem {
|
|
66
|
+
return {
|
|
67
|
+
label: item.label,
|
|
68
|
+
icon: item.icon,
|
|
69
|
+
color: item.color,
|
|
70
|
+
disabled: item.disabled || Boolean(item.disabledReason),
|
|
71
|
+
loading: item.loading,
|
|
72
|
+
to: item.to,
|
|
73
|
+
target: item.target,
|
|
74
|
+
onSelect: item.onSelect,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const menuGroups = computed<DropdownMenuItem[][]>(() => {
|
|
79
|
+
const collapsedAutos = collapsed.value
|
|
80
|
+
? props.items.filter(item => (item.visibility ?? 'auto') === 'auto')
|
|
81
|
+
: [];
|
|
82
|
+
|
|
83
|
+
// Menu-only items grouped by section, in first-appearance order.
|
|
84
|
+
const sections = new Map<string, PageActionsItem[]>();
|
|
85
|
+
for (const item of props.items) {
|
|
86
|
+
if ((item.visibility ?? 'auto') !== 'menu') continue;
|
|
87
|
+
const section = item.section ?? 'default';
|
|
88
|
+
if (!sections.has(section)) sections.set(section, []);
|
|
89
|
+
sections.get(section)!.push(item);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const utilityGroup: DropdownMenuItem[] = collapsed.value
|
|
93
|
+
? [
|
|
94
|
+
...props.utilityItems.map(toMenuItem),
|
|
95
|
+
...(showHelp.value && helpPanel
|
|
96
|
+
? [{ label: t('pageChrome.help'), icon: 'i-lucide-circle-help', onSelect: () => helpPanel.toggle() }]
|
|
97
|
+
: []),
|
|
98
|
+
]
|
|
99
|
+
: [];
|
|
100
|
+
|
|
101
|
+
return [
|
|
102
|
+
collapsedAutos.map(toMenuItem),
|
|
103
|
+
...[...sections.values()].map(group => group.map(toMenuItem)),
|
|
104
|
+
utilityGroup,
|
|
105
|
+
].filter(group => group.length > 0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const hasUtilityRegion = computed(() =>
|
|
109
|
+
inlineUtilityItems.value.length > 0 || (showHelp.value && !collapsed.value),
|
|
110
|
+
);
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<template>
|
|
114
|
+
<PageAction
|
|
115
|
+
v-for="item in inlineItems"
|
|
116
|
+
:key="item.key"
|
|
117
|
+
:icon="item.icon"
|
|
118
|
+
:label="item.label"
|
|
119
|
+
show-label
|
|
120
|
+
:tone="item.tone ?? 'neutral'"
|
|
121
|
+
:loading="item.loading"
|
|
122
|
+
:disabled="item.disabled"
|
|
123
|
+
:disabled-reason="item.disabledReason"
|
|
124
|
+
:to="item.to"
|
|
125
|
+
:target="item.target"
|
|
126
|
+
@click="item.onSelect?.()"
|
|
127
|
+
/>
|
|
128
|
+
<PageActionMenu :items="menuGroups" />
|
|
129
|
+
<template v-if="hasUtilityRegion">
|
|
130
|
+
<USeparator orientation="vertical" class="h-5 mx-1" />
|
|
131
|
+
<PageAction
|
|
132
|
+
v-for="item in inlineUtilityItems"
|
|
133
|
+
:key="item.key"
|
|
134
|
+
:icon="item.icon"
|
|
135
|
+
:label="item.label"
|
|
136
|
+
show-label
|
|
137
|
+
:loading="item.loading"
|
|
138
|
+
:disabled="item.disabled"
|
|
139
|
+
:to="item.to"
|
|
140
|
+
:target="item.target"
|
|
141
|
+
@click="item.onSelect?.()"
|
|
142
|
+
/>
|
|
143
|
+
<PageAction
|
|
144
|
+
v-if="showHelp && helpPanel"
|
|
145
|
+
icon="i-lucide-circle-help"
|
|
146
|
+
:label="t('pageChrome.help')"
|
|
147
|
+
show-label
|
|
148
|
+
@click="helpPanel.toggle()"
|
|
149
|
+
/>
|
|
150
|
+
</template>
|
|
151
|
+
</template>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Shipped as source: the consumer's Vite compiles this SFC. All imports are
|
|
3
3
|
// explicit — no reliance on the consumer's auto-import configuration.
|
|
4
4
|
// i18n key contract: pageChrome.back (+ PageActionMenu/PageUtilityActions keys).
|
|
5
|
-
import { computed } from 'vue'
|
|
5
|
+
import { computed, onBeforeUnmount, onMounted, provide, ref } from 'vue'
|
|
6
6
|
import { useI18n } from 'vue-i18n'
|
|
7
7
|
import { useRouter, type RouteLocationRaw } from 'vue-router'
|
|
8
8
|
import UButton from '@nuxt/ui/components/Button.vue'
|
|
@@ -10,6 +10,7 @@ import USkeleton from '@nuxt/ui/components/Skeleton.vue'
|
|
|
10
10
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
11
11
|
import PageActionMenu from './PageActionMenu.vue'
|
|
12
12
|
import PageUtilityActions from './PageUtilityActions.vue'
|
|
13
|
+
import { PAGE_HEADER_WIDTH } from './pageActions.ts'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Standard page header.
|
|
@@ -27,6 +28,11 @@ import PageUtilityActions from './PageUtilityActions.vue'
|
|
|
27
28
|
* - Header height, spacing, and tooltip behavior are normalized via `PageAction`.
|
|
28
29
|
* - Utility triggers (Help, AI history, …) live in `#utility` (default =
|
|
29
30
|
* `PageUtilityActions`) and render as LABELED buttons, not icon-only.
|
|
31
|
+
*
|
|
32
|
+
* Prefer driving all of the above declaratively with `PageActions` (place it
|
|
33
|
+
* in `#actions`, pass `:utility="false"`): it enforces the conventions and
|
|
34
|
+
* collapses 'auto'/utility items into the ⋯ menu on narrow headers using the
|
|
35
|
+
* width this component provides via `PAGE_HEADER_WIDTH`.
|
|
30
36
|
*/
|
|
31
37
|
const props = withDefaults(defineProps<{
|
|
32
38
|
title?: string
|
|
@@ -56,6 +62,22 @@ const props = withDefaults(defineProps<{
|
|
|
56
62
|
const { t } = useI18n()
|
|
57
63
|
const router = useRouter()
|
|
58
64
|
|
|
65
|
+
// Measured content width for PageActions' collapse decision. null until the
|
|
66
|
+
// first observation (treated as wide — the flex-wrap fallback covers it).
|
|
67
|
+
const wrapperEl = ref<HTMLElement | null>(null)
|
|
68
|
+
const headerWidth = ref<number | null>(null)
|
|
69
|
+
provide(PAGE_HEADER_WIDTH, headerWidth)
|
|
70
|
+
|
|
71
|
+
let observer: ResizeObserver | null = null
|
|
72
|
+
onMounted(() => {
|
|
73
|
+
if (!wrapperEl.value || typeof ResizeObserver === 'undefined') return
|
|
74
|
+
observer = new ResizeObserver((entries) => {
|
|
75
|
+
headerWidth.value = entries[0]?.contentRect.width ?? null
|
|
76
|
+
})
|
|
77
|
+
observer.observe(wrapperEl.value)
|
|
78
|
+
})
|
|
79
|
+
onBeforeUnmount(() => observer?.disconnect())
|
|
80
|
+
|
|
59
81
|
function onBack() {
|
|
60
82
|
if (typeof props.back === 'object' && props.back && 'to' in props.back) {
|
|
61
83
|
router.push(props.back.to)
|
|
@@ -80,7 +102,7 @@ const titleClass = computed(() => props.density === 'compact' ? 'font-display te
|
|
|
80
102
|
</script>
|
|
81
103
|
|
|
82
104
|
<template>
|
|
83
|
-
<div :class="wrapperClass">
|
|
105
|
+
<div ref="wrapperEl" :class="wrapperClass">
|
|
84
106
|
<UButton
|
|
85
107
|
v-if="back"
|
|
86
108
|
icon="i-lucide-arrow-left"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue';
|
|
2
|
+
import type { RouteLocationRaw } from 'vue-router';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Declarative page-header action. One array describes every action of a page —
|
|
6
|
+
* inline buttons and overflow-menu items alike — and `PageActions` decides
|
|
7
|
+
* placement from `visibility` and the available header width.
|
|
8
|
+
*/
|
|
9
|
+
export interface PageActionsItem {
|
|
10
|
+
/** Stable identity (also the Vue key). */
|
|
11
|
+
key: string;
|
|
12
|
+
icon: string;
|
|
13
|
+
label: string;
|
|
14
|
+
/** Inline button tone. At most ONE 'primary' item should be visible per state. */
|
|
15
|
+
tone?: 'primary' | 'neutral';
|
|
16
|
+
/**
|
|
17
|
+
* Placement tier:
|
|
18
|
+
* - 'always' — inline at every width (the state's main action)
|
|
19
|
+
* - 'auto' — inline when the header is wide enough, else moved into the
|
|
20
|
+
* overflow menu (default)
|
|
21
|
+
* - 'menu' — overflow-menu only
|
|
22
|
+
*/
|
|
23
|
+
visibility?: 'always' | 'auto' | 'menu';
|
|
24
|
+
/**
|
|
25
|
+
* Menu group id for 'menu' items (default 'default'). Groups render as
|
|
26
|
+
* separated menu sections in first-appearance order; collapsed 'auto' items
|
|
27
|
+
* form the leading group and utility items the trailing one. Convention:
|
|
28
|
+
* put destructive rows in the last-declared section.
|
|
29
|
+
*/
|
|
30
|
+
section?: string;
|
|
31
|
+
/** Menu-item color (e.g. 'error' for destructive rows). Inline tone wins inline. */
|
|
32
|
+
color?: 'error' | 'warning';
|
|
33
|
+
loading?: boolean;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** Blocked-with-reason: renders disabled; inline buttons tooltip "label — reason". */
|
|
36
|
+
disabledReason?: string | null;
|
|
37
|
+
to?: RouteLocationRaw;
|
|
38
|
+
target?: string;
|
|
39
|
+
onSelect?: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Current PageHeader content width in px, provided by `PageHeader` via a
|
|
44
|
+
* ResizeObserver. `null` until first measurement (treated as wide).
|
|
45
|
+
*/
|
|
46
|
+
export const PAGE_HEADER_WIDTH: InjectionKey<Ref<number | null>> = Symbol('page-header-width');
|
|
47
|
+
|
|
48
|
+
/** Below this header width (px), 'auto' actions and utilities collapse into the menu. */
|
|
49
|
+
export const PAGE_ACTIONS_COLLAPSE_BELOW = 640;
|