@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 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.9.1",
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;