@bagelink/vue 1.15.125 → 1.15.129
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/components/form/inputs/RichText/index.vue.d.ts +1 -0
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/layout/AppContent.vue.d.ts +64 -0
- package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
- package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
- package/dist/components/layout/appLayoutContext.d.ts +6 -0
- package/dist/components/layout/appLayoutContext.d.ts.map +1 -1
- package/dist/index.cjs +137 -164
- package/dist/index.mjs +35255 -29163
- package/dist/style.css +2 -1
- package/package.json +1 -1
- package/src/components/FloatingPanel.vue +1 -1
- package/src/components/ListItem.vue +1 -1
- package/src/components/Menu.vue +1 -1
- package/src/components/form/inputs/RichText/index.vue +41 -1
- package/src/components/layout/AppContent.vue +349 -26
- package/src/components/layout/AppLayout.vue +14 -2
- package/src/components/layout/appLayoutContext.ts +9 -0
- package/src/styles/layout.css +10 -0
package/package.json
CHANGED
|
@@ -88,7 +88,7 @@ defineExpose({
|
|
|
88
88
|
height: min(90vh, 800px);
|
|
89
89
|
margin: 1rem;
|
|
90
90
|
border-radius: 12px;
|
|
91
|
-
border: 1px solid var(--bgl-
|
|
91
|
+
border: 1px solid var(--bgl-border-color, #e0e0e0);
|
|
92
92
|
background: var(--bgl-box-bg, #fff);
|
|
93
93
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.15);
|
|
94
94
|
overflow: hidden;
|
|
@@ -192,7 +192,7 @@ inset-inline-end: var(--bgl-list-item-end-inset);
|
|
|
192
192
|
z-index: 1;
|
|
193
193
|
/* non-interactive meta lets clicks fall through to the row button… */
|
|
194
194
|
pointer-events: none;
|
|
195
|
-
padding-inline-end:
|
|
195
|
+
padding-inline-end: 0.25rem;
|
|
196
196
|
}
|
|
197
197
|
/* …interactive trailing controls stay clickable on top of the row button. */
|
|
198
198
|
.list-item-fullrow .list-item-end :where(button, a, input, select, label, [role="button"], [tabindex]) {
|
package/src/components/Menu.vue
CHANGED
|
@@ -7,7 +7,7 @@ defineProps<{ items: NavLink[] }>()
|
|
|
7
7
|
</script>
|
|
8
8
|
|
|
9
9
|
<template>
|
|
10
|
-
<div class="flex space-between m_w-100vw overflow m_-ms-1 m_-me-1 m_px-1 m_pt-025 m_pb-
|
|
10
|
+
<div class="flex space-between m_w-100vw overflow-x overflow-y-hidden m_-ms-1 m_-me-1 m_px-1 m_pt-025 m_pb-0 bglTopMenu">
|
|
11
11
|
<div class="flex gap-025 m_gap-05">
|
|
12
12
|
<template v-for="item in items" :key="item.label">
|
|
13
13
|
<Dropdown v-if="item.children" :value="resolveI18n(item.label)" iconEnd="keyboard_arrow_down">
|
|
@@ -26,6 +26,8 @@ const props = defineProps<{
|
|
|
26
26
|
debug?: boolean
|
|
27
27
|
label?: string
|
|
28
28
|
height?: number | string
|
|
29
|
+
// Make the editor fill the full height of its parent container
|
|
30
|
+
fullHeight?: boolean
|
|
29
31
|
basic?: boolean
|
|
30
32
|
simple?: boolean
|
|
31
33
|
placeholder?: string
|
|
@@ -2119,6 +2121,11 @@ const hasRTL = computed(() => RTL_REGEX.test(props.modelValue))
|
|
|
2119
2121
|
|
|
2120
2122
|
// Computed property to handle height prop
|
|
2121
2123
|
const editorHeight = computed(() => {
|
|
2124
|
+
// When fullHeight is enabled the editor stretches to fill its parent,
|
|
2125
|
+
// so we don't impose a fixed pixel height.
|
|
2126
|
+
if (props.fullHeight) {
|
|
2127
|
+
return '100%'
|
|
2128
|
+
}
|
|
2122
2129
|
if (typeof props.height === 'number') {
|
|
2123
2130
|
return `${props.height}px`
|
|
2124
2131
|
} if (typeof props.height === 'string') {
|
|
@@ -3267,7 +3274,7 @@ const imgTransform = computed({
|
|
|
3267
3274
|
|
|
3268
3275
|
<template>
|
|
3269
3276
|
<div
|
|
3270
|
-
class="bagel-input" :class="[shellClass, { 'has-value': hasEditorValue }]" :style="shellStyle"
|
|
3277
|
+
class="bagel-input" :class="[shellClass, { 'has-value': hasEditorValue, 'rich-text-full-height': fullHeight }]" :style="shellStyle"
|
|
3271
3278
|
v-bind="attrs"
|
|
3272
3279
|
>
|
|
3273
3280
|
<label v-if="label">{{ label }}</label>
|
|
@@ -3278,6 +3285,7 @@ const imgTransform = computed({
|
|
|
3278
3285
|
'rich-text-editor pt-05 px-05 pb-075': !basic,
|
|
3279
3286
|
'rich-text-editor--basic': basic,
|
|
3280
3287
|
'fullscreen-mode': editor.state.isFullscreen,
|
|
3288
|
+
'full-height': fullHeight && !editor.state.isFullscreen,
|
|
3281
3289
|
},
|
|
3282
3290
|
]"
|
|
3283
3291
|
>
|
|
@@ -3867,6 +3875,38 @@ white-space: pre-wrap;
|
|
|
3867
3875
|
overflow-x: auto;
|
|
3868
3876
|
}
|
|
3869
3877
|
|
|
3878
|
+
/* Full-height mode: the editor stretches to fill its parent container.
|
|
3879
|
+
The whole chain (input shell → editor → container → content-area → iframe)
|
|
3880
|
+
must be a flex column so the iframe can grow to occupy the remaining space. */
|
|
3881
|
+
.rich-text-full-height {
|
|
3882
|
+
display: flex;
|
|
3883
|
+
flex-direction: column;
|
|
3884
|
+
height: 100%;
|
|
3885
|
+
min-height: 0;
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
.rich-text-full-height > .full-height {
|
|
3889
|
+
flex: 1;
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
.full-height {
|
|
3893
|
+
display: flex;
|
|
3894
|
+
flex-direction: column;
|
|
3895
|
+
height: 100%;
|
|
3896
|
+
min-height: 0;
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
.full-height .editor-container {
|
|
3900
|
+
flex: 1;
|
|
3901
|
+
min-height: 0;
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
.full-height .content-area {
|
|
3905
|
+
height: 100%;
|
|
3906
|
+
min-height: 0;
|
|
3907
|
+
overflow-y: auto;
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3870
3910
|
.fullscreen-mode {
|
|
3871
3911
|
position: fixed;
|
|
3872
3912
|
top: 0;
|
|
@@ -1,20 +1,85 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, useSlots } from 'vue'
|
|
3
3
|
import { Btn, PageTitle } from '@bagelink/vue'
|
|
4
4
|
import { useAppLayout } from './appLayoutContext'
|
|
5
5
|
|
|
6
|
+
/** How the header is framed against the page body. */
|
|
7
|
+
type HeaderVariant = 'border' | 'card' | 'none'
|
|
8
|
+
|
|
6
9
|
interface Props {
|
|
7
10
|
title?: string
|
|
8
11
|
/** Secondary line under the title (page description / context). */
|
|
9
12
|
subtitle?: string
|
|
10
13
|
showMenuButton?: boolean
|
|
11
14
|
backTo?: string | Record<string, any>
|
|
15
|
+
/**
|
|
16
|
+
* Header framing:
|
|
17
|
+
* 'border' → bottom hairline under the header (default — legacy behaviour).
|
|
18
|
+
* 'card' → header sits as a rounded, elevated card above the body.
|
|
19
|
+
* 'none' → no border, no card (flush with the body).
|
|
20
|
+
*/
|
|
21
|
+
variant?: HeaderVariant
|
|
22
|
+
/**
|
|
23
|
+
* Override the header framing on mobile (≤ breakpoint) only. Lets a header be,
|
|
24
|
+
* say, a `card` on desktop but flush (`none`) on a phone, where the extra chrome
|
|
25
|
+
* and margins feel heavy. Unset (default) = mobile inherits the desktop variant.
|
|
26
|
+
*/
|
|
27
|
+
mobileVariant?: HeaderVariant
|
|
28
|
+
/**
|
|
29
|
+
* Shorthand for `variant="card"` — lets you write `<AppContent card>` instead
|
|
30
|
+
* of `<AppContent variant="card">`. Ignored when `variant` is passed explicitly.
|
|
31
|
+
*/
|
|
32
|
+
card?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated Use `variant` instead. `border` maps to
|
|
35
|
+
* `variant="border"` (true, the old default) or `variant="none"` (false).
|
|
36
|
+
* Only applied when `variant` (and `card`) are not passed.
|
|
37
|
+
*/
|
|
12
38
|
border?: boolean
|
|
39
|
+
/**
|
|
40
|
+
* Fixed header height. Locked in on both desktop and mobile so the header's
|
|
41
|
+
* top-start corner (and everything in it — titles, inputs, buttons) sits at
|
|
42
|
+
* the exact same place on every page, regardless of content. Any CSS length.
|
|
43
|
+
*/
|
|
44
|
+
headerHeight?: string
|
|
45
|
+
/**
|
|
46
|
+
* Lock the header to a single row of exactly `headerHeight` — content never
|
|
47
|
+
* wraps to a second line; anything that overflows is clipped. Off by default:
|
|
48
|
+
* the header keeps its fixed *minimum* height (stable start corner) but is
|
|
49
|
+
* free to grow downward into extra rows when content needs it (common on
|
|
50
|
+
* mobile). Turn this on only for headers you know must stay one exact line.
|
|
51
|
+
*/
|
|
52
|
+
singleRow?: boolean
|
|
53
|
+
/**
|
|
54
|
+
* Cap the readable width of the page and center it on wide screens. Applies to
|
|
55
|
+
* BOTH the header and the body, so the title stays aligned above the content
|
|
56
|
+
* (no more per-view `w-1100px mx-auto`). Any CSS length, e.g. '1100px', '80rem'.
|
|
57
|
+
* Unset (default) = full-bleed. On narrow screens the content simply fills the
|
|
58
|
+
* available width (the cap only kicks in once the viewport is wider than it).
|
|
59
|
+
*/
|
|
60
|
+
maxWidth?: string
|
|
61
|
+
/**
|
|
62
|
+
* Make the body's child fill the content area — the body becomes a flex column
|
|
63
|
+
* so its child takes the full width and the remaining height (`flex:1`) and owns
|
|
64
|
+
* its own scrolling. Saves repeating `h-100p w-100p overflow-hidden` on the
|
|
65
|
+
* wrapper of full-height views (tables, Kanban boards, chat, split panes). Off by
|
|
66
|
+
* default so short content (forms, empty states) still flows naturally at its own
|
|
67
|
+
* height instead of being stretched. Use only when the child should own the whole
|
|
68
|
+
* viewport region.
|
|
69
|
+
*/
|
|
70
|
+
fill?: boolean
|
|
13
71
|
}
|
|
14
72
|
|
|
15
|
-
withDefaults(defineProps<Props>(), {
|
|
73
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
16
74
|
showMenuButton: true,
|
|
17
|
-
|
|
75
|
+
variant: undefined,
|
|
76
|
+
mobileVariant: undefined,
|
|
77
|
+
card: false,
|
|
78
|
+
border: undefined,
|
|
79
|
+
headerHeight: '60px',
|
|
80
|
+
singleRow: false,
|
|
81
|
+
maxWidth: undefined,
|
|
82
|
+
fill: false,
|
|
18
83
|
})
|
|
19
84
|
|
|
20
85
|
const { isOpen, toggleMenu, regions } = useAppLayout()
|
|
@@ -22,25 +87,61 @@ const { isOpen, toggleMenu, regions } = useAppLayout()
|
|
|
22
87
|
// Only show the sidebar toggle when a sidebar actually exists (top/bottom-nav
|
|
23
88
|
// apps have no start region → the button would darken an empty backdrop).
|
|
24
89
|
const hasSidebar = computed(() => regions.value.start.present)
|
|
90
|
+
|
|
91
|
+
// Resolve the effective variant, in priority order: explicit `variant` wins; then
|
|
92
|
+
// the `card` shorthand; then the legacy `border` boolean; else the historical
|
|
93
|
+
// default of 'border'. (So `<AppContent card :border="false">` still yields a card.)
|
|
94
|
+
const resolvedVariant = computed<HeaderVariant>(() => {
|
|
95
|
+
if (props.variant) { return props.variant }
|
|
96
|
+
if (props.card) { return 'card' }
|
|
97
|
+
if (props.border !== undefined) { return props.border ? 'border' : 'none' }
|
|
98
|
+
return 'border'
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Mobile framing: an explicit `mobileVariant` overrides only below the breakpoint;
|
|
102
|
+
// otherwise mobile just inherits the resolved desktop variant. Both variant classes
|
|
103
|
+
// are always emitted (desktop + `--m-*`); the CSS media queries pick which applies.
|
|
104
|
+
const resolvedMobileVariant = computed<HeaderVariant>(() => props.mobileVariant ?? resolvedVariant.value)
|
|
105
|
+
|
|
106
|
+
// Lock the header to a single stable height on both breakpoints, and publish the
|
|
107
|
+
// optional content max-width. Both are inline vars so overrides stay trivial.
|
|
108
|
+
const headerVars = computed(() => ({
|
|
109
|
+
'--bgl-app-header-h': props.headerHeight,
|
|
110
|
+
...(props.maxWidth ? { '--bgl-app-max-width': props.maxWidth } : {}),
|
|
111
|
+
}))
|
|
112
|
+
|
|
113
|
+
// Only center/cap when a maxWidth was actually given (keeps default full-bleed).
|
|
114
|
+
const isContained = computed(() => !!props.maxWidth)
|
|
115
|
+
|
|
116
|
+
// The center slot only renders (and reserves space) when actually provided —
|
|
117
|
+
// an empty middle should never force the header to wrap awkwardly.
|
|
118
|
+
const slots = useSlots()
|
|
119
|
+
const hasCenter = computed(() => !!slots['header-center'])
|
|
120
|
+
|
|
25
121
|
</script>
|
|
26
122
|
|
|
27
123
|
<template>
|
|
28
|
-
<div class="app-content h-100p flex column">
|
|
29
|
-
<!-- Header
|
|
124
|
+
<div class="app-content h-100p flex column" :class="{ 'app-content--contained': isContained }" :style="headerVars">
|
|
125
|
+
<!-- Header — fixed *minimum* height on every breakpoint so its start
|
|
126
|
+
corner sits in the exact same spot on every page. When content needs
|
|
127
|
+
more room (e.g. mobile) it wraps downward from that floor instead of
|
|
128
|
+
being clipped. Use `singleRow` to hard-lock to one exact line. -->
|
|
30
129
|
<header
|
|
31
|
-
class="app-header flex
|
|
32
|
-
:class="
|
|
130
|
+
class="app-header flex w-100p"
|
|
131
|
+
:class="[
|
|
132
|
+
`app-header--${resolvedVariant}`,
|
|
133
|
+
`app-header--m-${resolvedMobileVariant}`,
|
|
134
|
+
{ 'app-header--single-row': singleRow },
|
|
135
|
+
]"
|
|
33
136
|
>
|
|
34
137
|
<!-- Left Side -->
|
|
35
|
-
<div class="
|
|
138
|
+
<div class="app-header-side flex gap-col-075 min-w-0">
|
|
36
139
|
<!-- Menu Toggle Button -->
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
/>
|
|
43
|
-
</div>
|
|
140
|
+
<Btn
|
|
141
|
+
v-if="showMenuButton && hasSidebar" flat icon="dock_to_right" class="menuToggleButton"
|
|
142
|
+
:aria-expanded="isOpen" aria-controls="bgl-app-sidebar" aria-label="Toggle sidebar"
|
|
143
|
+
@click="toggleMenu"
|
|
144
|
+
/>
|
|
44
145
|
|
|
45
146
|
<!-- Back Button -->
|
|
46
147
|
<Btn v-if="backTo" icon="arrow_back" thin :to="backTo" class="back-btn bg-bg m_-ms-05 m_-me-05 color-black" />
|
|
@@ -54,20 +155,27 @@ const hasSidebar = computed(() => regions.value.start.present)
|
|
|
54
155
|
<slot name="header-left" />
|
|
55
156
|
</div>
|
|
56
157
|
|
|
57
|
-
<!-- Center Content -->
|
|
58
|
-
<div class="flex
|
|
158
|
+
<!-- Center Content (only rendered when the slot is provided) -->
|
|
159
|
+
<div v-if="hasCenter" class="app-header-center flex min-w-0">
|
|
59
160
|
<slot name="header-center" />
|
|
60
161
|
</div>
|
|
61
162
|
|
|
62
163
|
<!-- Right Side -->
|
|
63
|
-
<div class="
|
|
164
|
+
<div class="app-header-side flex gap-row-05 endNavTools">
|
|
64
165
|
<slot name="header-right" />
|
|
65
166
|
</div>
|
|
66
167
|
</header>
|
|
67
168
|
|
|
68
|
-
<!-- Page Content
|
|
169
|
+
<!-- Page Content — the gap under the header follows the variant: a border
|
|
170
|
+
earns a bit of breathing room, but a borderless (none/card) header
|
|
171
|
+
sits flush so the body hugs it. -->
|
|
69
172
|
<main
|
|
70
|
-
class="pageContent flex-grow overflow
|
|
173
|
+
class="pageContent flex-grow overflow pb-05 w-100p m_pb-05 m_scrollbar-gutter-auto"
|
|
174
|
+
:class="[
|
|
175
|
+
`pageContent--${resolvedVariant}`,
|
|
176
|
+
`pageContent--m-${resolvedMobileVariant}`,
|
|
177
|
+
{ 'pageContent--fill': fill },
|
|
178
|
+
]"
|
|
71
179
|
>
|
|
72
180
|
<slot name="content">
|
|
73
181
|
<!-- Default slot for content without explicit template -->
|
|
@@ -79,10 +187,18 @@ const hasSidebar = computed(() => regions.value.start.present)
|
|
|
79
187
|
|
|
80
188
|
<style>
|
|
81
189
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
190
|
+
/* Inputs dropped straight into a header slot default to a natural, compact width
|
|
191
|
+
instead of the input wrapper's global width:100% — otherwise a bare
|
|
192
|
+
<TextInput> (no width class) tries to fill the whole side and gets bumped onto
|
|
193
|
+
its own row. Views that DO want a specific width just add a utility: w-200px /
|
|
194
|
+
w-280px set max-width (compose fine with width:auto), and full-bleed cases use
|
|
195
|
+
m_w-1000px (mobile, where this desktop rule doesn't apply). Desktop only. */
|
|
196
|
+
@media screen and (min-width: 911px) {
|
|
197
|
+
.app-header .bagel-input {
|
|
198
|
+
width: auto;
|
|
199
|
+
min-width: 220px;
|
|
200
|
+
max-width: 100%;
|
|
201
|
+
}
|
|
86
202
|
}
|
|
87
203
|
|
|
88
204
|
.slide-fade-enter-from:has(.app-header) .pageContent,
|
|
@@ -97,16 +213,223 @@ transition: all 0.15s ease-in;
|
|
|
97
213
|
overflow: hidden;
|
|
98
214
|
}
|
|
99
215
|
|
|
216
|
+
/* Gutter ownership, breakpoint-aware (see AppLayout `.page-content`):
|
|
217
|
+
• Desktop — `.page-content` supplies the gutter, so an AppContent nested in it drops
|
|
218
|
+
its OWN inline padding to avoid doubling; header + body align with the parent edge.
|
|
219
|
+
• Mobile — `.page-content` drops its gutter, so AppContent keeps its own padding
|
|
220
|
+
(declared in the scoped block): the header runs edge-to-edge (its border reaches
|
|
221
|
+
the screen) while its content stays inset. A standalone AppContent (no
|
|
222
|
+
`.page-content` ancestor) always keeps its own padding — this rule simply doesn't
|
|
223
|
+
match there. Unscoped because `.page-content` belongs to AppLayout's scope. */
|
|
224
|
+
@media screen and (min-width: 911px) {
|
|
225
|
+
/* Repeated class (`.app-content.app-content`) lifts specificity above AppContent's
|
|
226
|
+
own scoped `.app-header[data-v]` / `.pageContent[data-v]` padding rule (which is
|
|
227
|
+
also two-specificity), so this reliably wins and zeroes the doubled gutter. */
|
|
228
|
+
.page-content .app-content.app-content .app-header,
|
|
229
|
+
.page-content .app-content.app-content .pageContent {
|
|
230
|
+
padding-inline: 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
100
234
|
</style>
|
|
101
235
|
|
|
102
236
|
<style scoped>
|
|
103
237
|
|
|
104
|
-
|
|
105
|
-
|
|
238
|
+
/* Both the header and the body carry the side gutter themselves (AppLayout's
|
|
239
|
+
`.page-content` no longer applies it). They read the same `--bgl-content-padding`
|
|
240
|
+
so their leading edges line up. The header keeps this padding on its OWN element
|
|
241
|
+
while its bottom border spans the full element width — since `.page-content` is
|
|
242
|
+
now flush to the screen edge, that border reaches the screen edge for free (the
|
|
243
|
+
old negative-margin hack is gone). The body insets its content by the same gutter. */
|
|
244
|
+
.pageContent {
|
|
245
|
+
padding-inline: var(--bgl-content-padding, 1rem);
|
|
106
246
|
}
|
|
107
247
|
|
|
248
|
+
/* The header holds a fixed *minimum* height on every breakpoint — its start
|
|
249
|
+
corner never shifts between pages, whatever it holds (title, input, buttons),
|
|
250
|
+
because the floor is identical everywhere. When content genuinely needs more
|
|
251
|
+
room (long toolbars, small screens) it grows downward from that same floor
|
|
252
|
+
rather than being clipped — the leading corner stays put, only the height
|
|
253
|
+
below it expands. Inline padding aligns to the page's content gutter so the
|
|
254
|
+
header's leading edge lines up with the body. */
|
|
108
255
|
.app-header {
|
|
109
256
|
flex-shrink: 0;
|
|
257
|
+
box-sizing: border-box;
|
|
258
|
+
min-height: var(--bgl-app-header-h, 60px);
|
|
259
|
+
gap: 0.5rem 0.75rem;
|
|
260
|
+
padding-inline: var(--bgl-content-padding, 1rem);
|
|
261
|
+
padding-block: 0.5rem;
|
|
262
|
+
flex-wrap: wrap;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Mobile: headers wrap to multiple rows far more often (narrow viewport), so the
|
|
266
|
+
between-row gap is tightened — the airy 0.5rem that reads fine as a rare desktop
|
|
267
|
+
wrap looks like dead space when the header routinely stacks on a phone. */
|
|
268
|
+
@media screen and (max-width: 910px) {
|
|
269
|
+
.app-header {
|
|
270
|
+
row-gap: 0.25rem;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* Each side hugs its own content — it does NOT grow, so inputs and toolbars keep
|
|
275
|
+
their natural size instead of being stretched to fill the row (which would
|
|
276
|
+
otherwise push a full-width input onto its own line). When the sides can't
|
|
277
|
+
share one row they wrap onto extra rows; min-width:0 + flex-wrap let a single
|
|
278
|
+
over-wide side shrink/wrap its own children rather than spill out. The
|
|
279
|
+
row-gap gives that WITHIN-side wrapping (e.g. a toolbar of buttons breaking
|
|
280
|
+
onto two lines) extra breathing room between its rows — mobile more so. */
|
|
281
|
+
.app-header-side {
|
|
282
|
+
flex: 0 1 auto;
|
|
283
|
+
min-width: 0;
|
|
284
|
+
flex-wrap: wrap;
|
|
285
|
+
max-width: 100%;
|
|
286
|
+
row-gap: 0.5rem;
|
|
287
|
+
}
|
|
288
|
+
/* Same tightening for wrapping WITHIN a side (e.g. a toolbar of buttons breaking
|
|
289
|
+
onto two lines) on mobile. */
|
|
290
|
+
@media screen and (max-width: 910px) {
|
|
291
|
+
.app-header-side {
|
|
292
|
+
row-gap: 0.25rem;
|
|
293
|
+
flex-grow: 1;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* The trailing toolbar is pushed to the end of a shared row by an auto margin
|
|
298
|
+
(no flex growth needed, so it never steals width from the leading side). When
|
|
299
|
+
it can't fit and wraps onto its own line the auto margin collapses and it sits
|
|
300
|
+
at the start of that row; justify-content keeps its actions end-aligned either
|
|
301
|
+
way. This holds on every breakpoint — the sides stay on one row whenever they
|
|
302
|
+
fit (the common case: compact left + 1-2 buttons right) and only wrap when the
|
|
303
|
+
content genuinely doesn't fit. A search input that wants a full-width row on
|
|
304
|
+
mobile opts in per-view with `m_w-1000px` ON THE INPUT (its width then exceeds
|
|
305
|
+
the row and it wraps), rather than the header force-stacking every left side. */
|
|
306
|
+
.endNavTools {
|
|
307
|
+
margin-inline-start: auto;
|
|
308
|
+
justify-content: flex-end;
|
|
309
|
+
}
|
|
310
|
+
/* Mobile: don't push the trailing side to the far edge — let it sit right after the
|
|
311
|
+
leading side (no auto margin, start-aligned) so the header reads as one compact
|
|
312
|
+
group instead of splitting title and actions to opposite screen edges. */
|
|
313
|
+
@media screen and (max-width: 910px) {
|
|
314
|
+
.endNavTools {
|
|
315
|
+
margin-inline-start: 0;
|
|
316
|
+
justify-content: flex-start;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ── Variants ──────────────────────────────────────────────────────────────
|
|
321
|
+
border → hairline under the header (legacy default)
|
|
322
|
+
card → header floats as a rounded, elevated card above the body
|
|
323
|
+
none → flush, no chrome
|
|
324
|
+
|
|
325
|
+
The desktop variant classes are scoped to the wide breakpoint and the mobile
|
|
326
|
+
`--m-*` classes to the narrow one, so a header can carry both (e.g. card on
|
|
327
|
+
desktop, none on mobile) and the right one wins purely by media query — no
|
|
328
|
+
specificity juggling. Shared framing lives in a mixin-style duplicate below. */
|
|
329
|
+
@media screen and (min-width: 911px) {
|
|
330
|
+
.app-header--border {
|
|
331
|
+
border-bottom: 1px solid var(--bgl-border-color);
|
|
332
|
+
}
|
|
333
|
+
.app-header--card {
|
|
334
|
+
background: var(--bgl-box-bg);
|
|
335
|
+
border: 1px solid var(--bgl-border-color);
|
|
336
|
+
border-radius: var(--bgl-radius, 0.75rem);
|
|
337
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
338
|
+
margin: 0.5rem;
|
|
339
|
+
padding-inline: 0.75rem;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
@media screen and (max-width: 910px) {
|
|
343
|
+
.app-header--m-border {
|
|
344
|
+
border-bottom: 1px solid var(--bgl-border-color);
|
|
345
|
+
}
|
|
346
|
+
.app-header--m-card {
|
|
347
|
+
background: var(--bgl-box-bg);
|
|
348
|
+
border: 1px solid var(--bgl-border-color);
|
|
349
|
+
border-radius: var(--bgl-radius, 0.75rem);
|
|
350
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
351
|
+
margin: 0.5rem;
|
|
352
|
+
padding-inline: 0.75rem;
|
|
353
|
+
}
|
|
354
|
+
/* `none` on mobile: make sure no inherited chrome lingers. */
|
|
355
|
+
.app-header--m-none {
|
|
356
|
+
border-bottom: 0;
|
|
357
|
+
background: none;
|
|
358
|
+
box-shadow: none;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* Hard-lock to a single row of exactly `headerHeight` — no wrapping, overflow
|
|
363
|
+
clipped. Turns the min-height floor into an exact height. */
|
|
364
|
+
.app-header--single-row {
|
|
365
|
+
height: var(--bgl-app-header-h, 60px);
|
|
366
|
+
flex-wrap: nowrap;
|
|
367
|
+
}
|
|
368
|
+
.app-header--single-row .app-header-side {
|
|
369
|
+
overflow: hidden;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* Contained mode (`maxWidth` prop): cap + center ONCE on the shared parent, so
|
|
373
|
+
the header and body inherit the same width and are aligned by construction —
|
|
374
|
+
no per-child centering margins to keep in sync. The effective width keeps a
|
|
375
|
+
safety gutter below the cap (`100% - 2*gutter`) and hits `maxWidth` above it.
|
|
376
|
+
The inner column is centered with a single auto margin. */
|
|
377
|
+
.app-content--contained {
|
|
378
|
+
width: min(100% - 2 * var(--bgl-content-padding, 1rem), var(--bgl-app-max-width, 100%));
|
|
379
|
+
margin-inline: auto;
|
|
380
|
+
}
|
|
381
|
+
/* Contained already supplies the side whitespace via the centered column, so the
|
|
382
|
+
header/body drop their own inline gutter and sit flush to the capped edge. */
|
|
383
|
+
.app-content--contained .app-header,
|
|
384
|
+
.app-content--contained .pageContent {
|
|
385
|
+
padding-inline: 0;
|
|
386
|
+
}
|
|
387
|
+
/* …except a card header, whose rounded frame still needs its inner inset. */
|
|
388
|
+
.app-content--contained .app-header--card {
|
|
389
|
+
padding-inline: 0.75rem;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* Gap under the header, per variant. A bordered header keeps some breathing room
|
|
393
|
+
below the hairline; a card keeps a small gap so the body doesn't touch the
|
|
394
|
+
card's frame; a borderless (none) header sits fully flush so the body hugs it.
|
|
395
|
+
Desktop and mobile are tuned separately — the mobile gaps are tighter (a phone
|
|
396
|
+
has little vertical room to spare, and a single-row header felt too airy). */
|
|
397
|
+
@media screen and (min-width: 911px) {
|
|
398
|
+
.pageContent--border {
|
|
399
|
+
padding-top: 1rem;
|
|
400
|
+
}
|
|
401
|
+
.pageContent--card {
|
|
402
|
+
padding-top: 0.25rem;
|
|
403
|
+
}
|
|
404
|
+
.pageContent--none {
|
|
405
|
+
padding-top: 0;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
@media screen and (max-width: 910px) {
|
|
409
|
+
.pageContent--m-border {
|
|
410
|
+
padding-top: 0.25rem;
|
|
411
|
+
}
|
|
412
|
+
.pageContent--m-card {
|
|
413
|
+
padding-top: 0.25rem;
|
|
414
|
+
}
|
|
415
|
+
.pageContent--m-none {
|
|
416
|
+
padding-top: 0;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* `fill` mode: the body becomes a flex column so its single child fills the whole
|
|
421
|
+
region (full width, remaining height) and owns its own scrolling — no need to
|
|
422
|
+
repeat `h-100p w-100p overflow-hidden` on the child. The `> *` targets the one
|
|
423
|
+
direct child; multiple children still each span full width and stack. */
|
|
424
|
+
.pageContent--fill {
|
|
425
|
+
display: flex;
|
|
426
|
+
flex-direction: column;
|
|
427
|
+
overflow: hidden;
|
|
428
|
+
}
|
|
429
|
+
.pageContent--fill > * {
|
|
430
|
+
flex: 1 1 auto;
|
|
431
|
+
min-height: 0;
|
|
432
|
+
width: 100%;
|
|
110
433
|
}
|
|
111
434
|
|
|
112
435
|
main {
|
|
@@ -318,11 +318,23 @@ transition: margin 300ms ease;
|
|
|
318
318
|
min-height: 0;
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
/*
|
|
322
|
-
|
|
321
|
+
/* Content gutter, breakpoint-aware:
|
|
322
|
+
• Desktop — `.page-content` applies the side gutter (as it always did), so every
|
|
323
|
+
view (with or without AppContent) is inset and aligned. An AppContent header
|
|
324
|
+
therefore lines up with its body here; edge-to-edge is opt-in via `edgeHeader`.
|
|
325
|
+
• Mobile — the gutter is dropped from `.page-content` so an AppContent header can
|
|
326
|
+
run flush to the screen edge (full-width divider — the common mobile look). The
|
|
327
|
+
header/body still inset their CONTENT because AppContent applies the same
|
|
328
|
+
`--bgl-content-padding` on its own elements; direct-in-slot views keep their own
|
|
329
|
+
mobile spacing (`m_p-*`). The var stays the single gutter knob. */
|
|
323
330
|
.page-content {
|
|
324
331
|
overflow: auto;
|
|
325
332
|
padding-inline: var(--bgl-content-padding, 1rem);
|
|
326
333
|
}
|
|
334
|
+
@media screen and (max-width: 910px) {
|
|
335
|
+
.page-content {
|
|
336
|
+
padding-inline: 0;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
327
339
|
|
|
328
340
|
</style>
|
|
@@ -66,6 +66,15 @@ function inertRegions(): Ref<Record<RegionSide, RegionState>> {
|
|
|
66
66
|
})
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* True when called from inside a real `<AppLayout>` (a provider is present).
|
|
71
|
+
* Lets a child (e.g. AppContent) adapt its layout when it's nested in the shell
|
|
72
|
+
* vs. used standalone — without emitting the dev warning `useAppLayout` does.
|
|
73
|
+
*/
|
|
74
|
+
export function useIsInsideAppLayout(): boolean {
|
|
75
|
+
return inject(AppLayoutKey, null) !== null
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
79
|
* Access the AppLayout context (region state, sidebar open state, mobile flag).
|
|
71
80
|
* Works inside AppLayout's slots — including custom header buttons.
|
package/src/styles/layout.css
CHANGED
|
@@ -7765,4 +7765,14 @@
|
|
|
7765
7765
|
.mobile-only {
|
|
7766
7766
|
display: none !important;
|
|
7767
7767
|
}
|
|
7768
|
+
}
|
|
7769
|
+
|
|
7770
|
+
.page-gutter {
|
|
7771
|
+
/* The app's side gutter as a utility. AppLayout's `.page-content` no longer
|
|
7772
|
+
applies `padding-inline` itself; the content supplies it, so an AppContent
|
|
7773
|
+
header can sit flush to the screen edge. AppContent's body does this
|
|
7774
|
+
internally; a view rendered DIRECTLY in the layout slot (no AppContent
|
|
7775
|
+
wrapper) adds `.page-gutter` to its root for the same inset. Reads the
|
|
7776
|
+
`--bgl-content-padding` var from AppLayout (falls back to 1rem elsewhere). */
|
|
7777
|
+
padding-inline: var(--bgl-content-padding, 1rem);
|
|
7768
7778
|
}
|