@octabits-io/nuxt-ui-kit 0.16.0 → 0.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octabits-io/nuxt-ui-kit",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
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",
@@ -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, onBeforeUnmount, onMounted, provide, ref } from 'vue'
5
+ import { computed, onBeforeUnmount, onMounted, provide, ref, useSlots } 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'
@@ -43,7 +43,12 @@ const props = withDefaults(defineProps<{
43
43
  loading?: boolean
44
44
  /**
45
45
  * `default` = full-width top-of-page header with padding.
46
- * `compact` = sits inside a detail panel / sidebar; smaller title + thinner divider.
46
+ * `compact` = sits inside a detail panel / sidebar; ONE row, and it means it —
47
+ * smaller title, the subtitle beside it rather than under it, and
48
+ * padding sized to the action buttons instead of to two text
49
+ * lines. It used to differ from `default` only in title size and
50
+ * still spent `py-4` on a stacked title/subtitle, which on a
51
+ * split-pane view is 85px of chrome that never scrolls away.
47
52
  * `flush` = no padding/border (caller wraps it).
48
53
  */
49
54
  density?: 'default' | 'compact' | 'flush'
@@ -61,6 +66,7 @@ const props = withDefaults(defineProps<{
61
66
 
62
67
  const { t } = useI18n()
63
68
  const router = useRouter()
69
+ const slots = useSlots()
64
70
 
65
71
  // Measured content width for PageActions' collapse decision. null until the
66
72
  // first observation (treated as wide — the flex-wrap fallback covers it).
@@ -89,7 +95,7 @@ function onBack() {
89
95
  const wrapperClass = computed(() => {
90
96
  switch (props.density) {
91
97
  case 'compact':
92
- return 'flex items-center gap-2 flex-wrap border-b border-default px-6 py-4'
98
+ return 'flex items-center gap-2 flex-wrap border-b border-default px-6 py-2.5'
93
99
  case 'flush':
94
100
  return 'flex items-center gap-2 flex-wrap'
95
101
  case 'default':
@@ -98,7 +104,25 @@ const wrapperClass = computed(() => {
98
104
  }
99
105
  })
100
106
 
101
- const titleClass = computed(() => props.density === 'compact' ? 'font-display text-lg font-semibold tracking-tight' : 'font-display text-2xl font-semibold tracking-tight')
107
+ const titleClass = computed(() => props.density === 'compact' ? 'font-display text-base font-semibold tracking-tight' : 'font-display text-2xl font-semibold tracking-tight')
108
+
109
+ /**
110
+ * Title and subtitle side by side, on `compact` only, and only when this
111
+ * component is the one rendering them.
112
+ *
113
+ * A `#title` slot carries its own layout — the CMS detail panels put an icon
114
+ * beside a two-line block — and a baseline-aligned flex row would re-align it
115
+ * against text it does not contain. Slot users keep the plain wrapper they
116
+ * were laid out against; the change is for the prop path, which is the one
117
+ * that was stacking.
118
+ */
119
+ const inlineHeading = computed(() => props.density === 'compact' && !slots.title)
120
+ const headingClass = computed(() => inlineHeading.value
121
+ ? 'flex min-w-0 flex-wrap items-baseline gap-x-2'
122
+ : 'min-w-0')
123
+ const subtitleClass = computed(() => inlineHeading.value
124
+ ? 'text-sm text-muted min-w-0'
125
+ : 'text-sm text-muted mt-1')
102
126
  </script>
103
127
 
104
128
  <template>
@@ -113,12 +137,15 @@ const titleClass = computed(() => props.density === 'compact' ? 'font-display te
113
137
  @click="onBack"
114
138
  />
115
139
 
116
- <div class="min-w-0">
140
+ <!-- Skipped entirely when there is nothing to head with: a compact band
141
+ may legitimately carry actions alone (the page title is already in the
142
+ breadcrumb), and an empty box here would still take the `gap-2`. -->
143
+ <div v-if="$slots.title || title || subtitle || loading" :class="headingClass">
117
144
  <slot name="title">
118
145
  <USkeleton v-if="loading" class="h-7 w-40" />
119
146
  <h1 v-else-if="title" :class="titleClass">{{ title }}</h1>
120
147
  </slot>
121
- <p v-if="subtitle && !loading" class="text-sm text-muted mt-1">{{ subtitle }}</p>
148
+ <p v-if="subtitle && !loading" :class="subtitleClass">{{ subtitle }}</p>
122
149
  </div>
123
150
 
124
151
  <div v-if="$slots.badges" class="flex items-center gap-2">
@@ -58,6 +58,17 @@ watch(
58
58
 
59
59
  <template>
60
60
  <div class="flex h-full w-full flex-col overflow-hidden">
61
+ <!--
62
+ The page's name, for anyone not looking at the breadcrumb.
63
+
64
+ A split view names itself in the chrome above it, so the header slot no
65
+ longer repeats the title (it was the breadcrumb's string verbatim, one
66
+ row down, on fixed chrome that never scrolls away). That is a visual
67
+ economy, not a semantic one: the page still has a name, and with nothing
68
+ selected the detail column has no heading of its own to stand in for it.
69
+ -->
70
+ <h1 class="sr-only">{{ title }}</h1>
71
+
61
72
  <!-- Page header zone: rendered once (unlike #sidebar), full width above both columns -->
62
73
  <div v-if="$slots.header" class="shrink-0">
63
74
  <slot name="header" />