@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.
|
|
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;
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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="
|
|
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" />
|