@bagelink/vue 1.15.188 → 1.15.192
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/RadioGroup.vue.d.ts.map +1 -1
- package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
- package/dist/components/layout/AppLayout.vue.d.ts +37 -13
- package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
- package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
- package/dist/components/layout/SidebarNavItem.vue.d.ts.map +1 -1
- package/dist/components/layout/appLayoutContext.d.ts +22 -2
- package/dist/components/layout/appLayoutContext.d.ts.map +1 -1
- package/dist/form-flow/schema-fields.d.ts +11 -0
- package/dist/form-flow/schema-fields.d.ts.map +1 -1
- package/dist/index.cjs +31 -31
- package/dist/index.mjs +7507 -7437
- package/dist/style.css +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/form/inputs/RadioGroup.vue +39 -5
- package/src/components/layout/AppContent.vue +3 -1
- package/src/components/layout/AppLayout.vue +198 -45
- package/src/components/layout/AppSidebar.vue +114 -152
- package/src/components/layout/SidebarNavItem.vue +54 -73
- package/src/components/layout/appLayoutContext.ts +22 -2
- package/src/form-flow/schema-fields.ts +17 -2
- package/src/utils/index.ts +11 -4
package/dist/utils/index.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export { getOptionIcon, getOptionLabel, getOptionValue, normalizeOption } from '
|
|
|
46
46
|
export type { ComparisonOperator, FilterCondition, LogicalOperator, QueryConditions, QueryFilter } from './queryFilter';
|
|
47
47
|
export { anyOf, buildQuery, evaluateQuery, parseQuery, queryFilter, range, search } from './queryFilter';
|
|
48
48
|
export type { ShowdownConverter, ShowdownOptions } from './showdown';
|
|
49
|
+
/** Override the base URL used to resolve uploaded file path keys. */
|
|
50
|
+
export declare function setFileBaseUrl(baseUrl: string): void;
|
|
49
51
|
export declare function pathKeyToURL(pathKey?: string | null): string | undefined;
|
|
50
52
|
export declare function getNestedValue(obj: any, path?: string, defaultValue?: any): any;
|
|
51
53
|
export declare function tryRun<T>(func: () => T, callback?: (error: Error) => void): T | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts" generic="ContextObjType extends { [key: string]: any }">
|
|
2
2
|
import type { Option } from '@bagelink/vue'
|
|
3
3
|
import { Btn, Icon, resolveI18n, sanitizeHtml } from '@bagelink/vue'
|
|
4
|
-
import { computed, ref, watch } from 'vue'
|
|
4
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
5
5
|
import { normalizeOption } from '../../../utils/options'
|
|
6
6
|
import type { BagelInputShellProps } from './bagelInputShell'
|
|
7
7
|
import { useBagelInputShell } from './bagelInputShell'
|
|
@@ -84,6 +84,38 @@ const visibleOptions = computed<NormalizedRadioOption<ContextObjType>[]>(() => {
|
|
|
84
84
|
return raw.map(opt => normalizeOption<RadioOption<ContextObjType>>(opt as any) as unknown as NormalizedRadioOption<ContextObjType>)
|
|
85
85
|
})
|
|
86
86
|
|
|
87
|
+
const inlineWrapper = ref<HTMLElement | null>(null)
|
|
88
|
+
const inlineItemBasis = ref('90px')
|
|
89
|
+
let inlineResizeObserver: ResizeObserver | undefined
|
|
90
|
+
|
|
91
|
+
function updateInlineLayout() {
|
|
92
|
+
const wrapper = inlineWrapper.value
|
|
93
|
+
const itemCount = visibleOptions.value.length
|
|
94
|
+
if (!props.inline || !wrapper || !itemCount) { return }
|
|
95
|
+
|
|
96
|
+
const gap = Number.parseFloat(getComputedStyle(wrapper).columnGap) || 0
|
|
97
|
+
const minItemWidth = 100
|
|
98
|
+
const maxItemsPerRow = Math.max(1, Math.min(itemCount, Math.floor((wrapper.clientWidth + gap) / (minItemWidth + gap))))
|
|
99
|
+
const rowCount = Math.ceil(itemCount / maxItemsPerRow)
|
|
100
|
+
const itemsPerRow = Math.ceil(itemCount / rowCount)
|
|
101
|
+
const totalGap = gap * (itemsPerRow - 1)
|
|
102
|
+
|
|
103
|
+
inlineItemBasis.value = `calc((100% - ${totalGap}px) / ${itemsPerRow})`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
onMounted(() => {
|
|
107
|
+
inlineResizeObserver = new ResizeObserver(updateInlineLayout)
|
|
108
|
+
if (inlineWrapper.value) { inlineResizeObserver.observe(inlineWrapper.value) }
|
|
109
|
+
updateInlineLayout()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
onBeforeUnmount(() => inlineResizeObserver?.disconnect())
|
|
113
|
+
|
|
114
|
+
watch([() => visibleOptions.value.length, () => props.inline], async () => {
|
|
115
|
+
await nextTick()
|
|
116
|
+
updateInlineLayout()
|
|
117
|
+
})
|
|
118
|
+
|
|
87
119
|
async function loadOptionsIfNeeded() {
|
|
88
120
|
if (typeof props.options === 'function') {
|
|
89
121
|
try {
|
|
@@ -144,7 +176,8 @@ function handleChange() {
|
|
|
144
176
|
<p v-if="label" :id="`${name}-label`" class="group-label">
|
|
145
177
|
{{ resolveI18n(label) }} <span v-if="required">*</span>
|
|
146
178
|
</p>
|
|
147
|
-
<div class="radio-group-wrap" :class="[wrapperClass, { 'radio-group-wrap--inline': inline }]"
|
|
179
|
+
<div ref="inlineWrapper" class="radio-group-wrap" :class="[wrapperClass, { 'radio-group-wrap--inline': inline }]"
|
|
180
|
+
:style="inline ? { '--rg-item-basis': inlineItemBasis } : undefined">
|
|
148
181
|
<label v-for="(opt, index) in visibleOptions" :key="opt.id || `${name}-${index}`" class="border rounded flex active-list-item hover mb-05" :for="opt.id || `${name}-${index}`"
|
|
149
182
|
:class="{ 'p-05 gap-025': thin, 'py-1 gap-075': !thin, 'ps-05': !hideRadio, 'px-1': hideRadio && !thin, 'bg-gray-light': !bgColor && !flat, 'align-items-start': align === 'start' || align === 'top', 'align-items-center': align === 'center', 'align-items-end': align === 'end' || align === 'bottom', invertedActive, 'is-option-disabled': disabled || opt.disabled }"
|
|
150
183
|
:style="{ backgroundColor: bgColor, borderColor }">
|
|
@@ -154,9 +187,9 @@ function handleChange() {
|
|
|
154
187
|
'hideRadio': hideRadio,
|
|
155
188
|
}" @focus="handleFocus" @blur="handleBlur" @change="handleChange">
|
|
156
189
|
<div class="flex w-100 gap-1 flex-wrap m_gap-05 m_gap-row-025"
|
|
157
|
-
:class="{ 'txt-center justify-content-center': textAlign === 'center', 'txt-end justify-content-end': textAlign === 'right', 'txt-start justify-content-start': textAlign === 'left' }"
|
|
190
|
+
:class="{ 'txt-center justify-content-center mx-auto': textAlign === 'center', 'txt-end justify-content-end': textAlign === 'right', 'txt-start justify-content-start': textAlign === 'left' }"
|
|
158
191
|
:style="{ color: textColor }">
|
|
159
|
-
<img v-if="opt.imgSrc" class="py-025 radius-05 m_w40 object-fit" height="40" width="40" :src="opt.imgSrc" :alt="opt.imgAlt">
|
|
192
|
+
<img v-if="opt.imgSrc" class="py-025 radius-05 m_w40 object-fit" height="40" width="40" :src="opt.imgSrc" :alt="opt.imgAlt || opt.label || ''">
|
|
160
193
|
<Icon v-else-if="opt.icon" :icon="opt.icon" :size="1" />
|
|
161
194
|
<div class="">
|
|
162
195
|
<div v-if="opt.label" class="m-0 m_txt-14 line-height-14" v-html="sanitizeHtml(opt.label)" />
|
|
@@ -208,7 +241,8 @@ gap: 0.5rem;
|
|
|
208
241
|
}
|
|
209
242
|
|
|
210
243
|
.radio-group-wrap--inline > label {
|
|
211
|
-
flex: 1 1
|
|
244
|
+
flex: 1 1 var(--rg-item-basis, 90px);
|
|
245
|
+
min-width: 0;
|
|
212
246
|
margin-bottom: 0 !important;
|
|
213
247
|
}
|
|
214
248
|
|
|
@@ -86,7 +86,9 @@ const { isOpen, toggleMenu, regions } = useAppLayout()
|
|
|
86
86
|
|
|
87
87
|
// Only show the sidebar toggle when a sidebar actually exists (top/bottom-nav
|
|
88
88
|
// apps have no start region → the button would darken an empty backdrop).
|
|
89
|
-
|
|
89
|
+
// Only a real navigation sidebar gets a menu-toggle — not a chat dock that merely
|
|
90
|
+
// occupies the `start` rail (role: 'dock'). Undefined role = sidebar (back-compat).
|
|
91
|
+
const hasSidebar = computed(() => regions.value.start.present && regions.value.start.role !== 'dock')
|
|
90
92
|
|
|
91
93
|
// Resolve the effective variant, in priority order: explicit `variant` wins; then
|
|
92
94
|
// the `card` shorthand; then the legacy `border` boolean; else the historical
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
0.5rem) insets .page-content — the single element common to every archetype, so
|
|
17
17
|
spacing is identical with or without AppContent. Use "0" for full-bleed.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Push rails are REAL in-flow boxes: a child (AppSidebar/ChatDock) reports
|
|
20
|
+
{present, open, mode:'push', size} via setStart/setEnd and simply fills the box
|
|
21
|
+
(width/height:100%). AppLayout owns the reserved width + the drag-resize handle;
|
|
22
|
+
the app never mirrors width by hand. Overlay rails (mobile sidebar drawer,
|
|
23
|
+
floating/full chat) report mode:'overlay' and position themselves fixed, reading
|
|
20
24
|
`--bgl-region-top` / `--bgl-region-bottom` to sit below/above the nav bands.
|
|
21
|
-
They report {present, open, mode, size} via the injected setEnd() — the layout
|
|
22
|
-
does the inset math; the app never mirrors width/open by hand.
|
|
23
25
|
|
|
24
26
|
Full-page views (e.g. /ai) are just routes; hide the dock there to avoid double UI.
|
|
25
27
|
-->
|
|
@@ -67,7 +69,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
67
69
|
contentPaddingMobile: '0.5rem',
|
|
68
70
|
})
|
|
69
71
|
|
|
70
|
-
const emit = defineEmits<{
|
|
72
|
+
const emit = defineEmits<{
|
|
73
|
+
'update:open': [value: boolean]
|
|
74
|
+
/** Fired live during a drag-resize of a resizable start/end region. */
|
|
75
|
+
resize: [payload: { side: RegionSide, size: string }]
|
|
76
|
+
}>()
|
|
71
77
|
// Explicit type breaks a vue-tsc dts circular-inference (TS7022) where `slots`
|
|
72
78
|
// is referenced inside the `regions` ref initializer below.
|
|
73
79
|
const slots: Slots = useSlots()
|
|
@@ -185,27 +191,108 @@ function slotHasContent(fn?: (...args: any[]) => any): boolean {
|
|
|
185
191
|
return true
|
|
186
192
|
})
|
|
187
193
|
}
|
|
188
|
-
|
|
194
|
+
// A side's slot renders real content right now. Applies to BOTH start and end so a
|
|
195
|
+
// rail auto-releases its reserved box when its child is `v-if`'d away (e.g. the dock
|
|
196
|
+
// hidden on the full-page /ai route) — without the child having to report present:false
|
|
197
|
+
// on unmount. AppSidebar always renders real content when mounted, so this is a no-op
|
|
198
|
+
// for a normal sidebar and only bites when a slot goes empty.
|
|
199
|
+
function sideHasContent(side: RegionSide): boolean {
|
|
200
|
+
if (side === 'start') { return slotHasContent(slots.start) || slotHasContent(slots.sidebar) }
|
|
201
|
+
if (side === 'end') { return slotHasContent(slots.end) || slotHasContent(slots.aside) }
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Region sizing ─────────────────────────────────────────────────────────────
|
|
206
|
+
// A push start/end region is a real IN-FLOW box in the row: it occupies `reserved`
|
|
207
|
+
// width and the child fills it. An overlay region (mobile drawer, floating chat)
|
|
208
|
+
// floats fixed and reserves nothing. `reserved` is the box's flex-basis — the center
|
|
209
|
+
// naturally fills whatever's left, so there's no margin trick anymore.
|
|
210
|
+
function isOverlay(side: RegionSide): boolean {
|
|
211
|
+
const r = regions.value[side]
|
|
212
|
+
// A region is an overlay when it says so, or when it's the sidebar on mobile
|
|
213
|
+
// (the drawer always slides over content there).
|
|
214
|
+
return r.mode === 'overlay' || (side === 'start' && isMobile.value)
|
|
215
|
+
}
|
|
189
216
|
|
|
190
|
-
// ── Center inset math — reserve room only for present + open + push regions ────
|
|
191
217
|
function reserved(side: RegionSide): string {
|
|
192
218
|
const r = regions.value[side]
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (side === 'end' && !endHasContent.value) { return '0' }
|
|
197
|
-
if (!r.present || overlay) { return '0' }
|
|
219
|
+
// A rail reserves nothing when its slot renders nothing (e.g. dock v-if'd away).
|
|
220
|
+
if ((side === 'start' || side === 'end') && !sideHasContent(side)) { return '0' }
|
|
221
|
+
if (!r.present || isOverlay(side)) { return '0' }
|
|
198
222
|
return r.open ? r.size : r.collapsedSize
|
|
199
223
|
}
|
|
200
224
|
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
225
|
+
// Is this side rendered as an in-flow reserved rail right now? (present, pushing).
|
|
226
|
+
function isFlow(side: RegionSide): boolean {
|
|
227
|
+
const r = regions.value[side]
|
|
228
|
+
if ((side === 'start' || side === 'end') && !sideHasContent(side)) { return false }
|
|
229
|
+
return r.present && !isOverlay(side)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// The row is a CSS grid: [start | center | end]. AppLayout owns the whole layout by
|
|
233
|
+
// declaring the track sizes here — the rails just fill their track (100%), the center
|
|
234
|
+
// takes the rest via minmax(0,1fr). Overlay regions are `fixed` (out of flow) so their
|
|
235
|
+
// track collapses to 0 and they float above instead. Animating grid-template-columns
|
|
236
|
+
// gives the open/close + resize slide for free.
|
|
237
|
+
const rowStyle = computed(() => ({
|
|
238
|
+
gridTemplateColumns: `${reserved('start')} minmax(0, 1fr) ${reserved('end')}`,
|
|
207
239
|
}))
|
|
208
240
|
|
|
241
|
+
// ── Region drag-resize (start/end only) ───────────────────────────────────────
|
|
242
|
+
// AppLayout owns the resize gesture: a child becomes resizable just by reporting
|
|
243
|
+
// `resizable`+`minSize`+`maxSize`. The handle is a thin grip on the in-flow box's
|
|
244
|
+
// inner edge; dragging mutates `regions[side].size` live (px), clamped to bounds.
|
|
245
|
+
const RESIZABLE_SIDES = ['start', 'end'] as const
|
|
246
|
+
type ResizableSide = (typeof RESIZABLE_SIDES)[number]
|
|
247
|
+
|
|
248
|
+
function parsePx(v: string | undefined, fallback: number): number {
|
|
249
|
+
if (!v) { return fallback }
|
|
250
|
+
const n = Number.parseFloat(v)
|
|
251
|
+
return Number.isFinite(n) ? n : fallback
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// A handle shows only for a present, open, in-flow, resizable region.
|
|
255
|
+
function handleVisible(side: ResizableSide): boolean {
|
|
256
|
+
const r = regions.value[side]
|
|
257
|
+
return !!r.resizable && r.open && isFlow(side)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let dragSide: ResizableSide | null = null
|
|
261
|
+
let dragStartX = 0
|
|
262
|
+
let dragStartW = 0
|
|
263
|
+
|
|
264
|
+
function onResizeMove(e: PointerEvent) {
|
|
265
|
+
if (!dragSide) { return }
|
|
266
|
+
const r = regions.value[dragSide]
|
|
267
|
+
const min = parsePx(r.minSize, 0)
|
|
268
|
+
const max = parsePx(r.maxSize, window.innerWidth)
|
|
269
|
+
// Physical sign: RTL flips the inline axis. A `start` rail grows as the pointer
|
|
270
|
+
// moves toward the end edge; an `end` rail grows toward the start edge.
|
|
271
|
+
const rtl = typeof document !== 'undefined' && document.dir === 'rtl'
|
|
272
|
+
const dx = e.clientX - dragStartX
|
|
273
|
+
// LTR: start grows with +dx, end grows with −dx. RTL inverts both.
|
|
274
|
+
const dirSign = rtl ? -1 : 1
|
|
275
|
+
const sideSign = dragSide === 'start' ? 1 : -1
|
|
276
|
+
const next = Math.min(max, Math.max(min, dragStartW + dx * dirSign * sideSign))
|
|
277
|
+
const size = `${next}px`
|
|
278
|
+
patch(dragSide, { size })
|
|
279
|
+
emit('resize', { side: dragSide, size })
|
|
280
|
+
}
|
|
281
|
+
function onResizeUp() {
|
|
282
|
+
dragSide = null
|
|
283
|
+
document.body.style.userSelect = ''
|
|
284
|
+
window.removeEventListener('pointermove', onResizeMove)
|
|
285
|
+
window.removeEventListener('pointerup', onResizeUp)
|
|
286
|
+
}
|
|
287
|
+
function onResizeDown(side: ResizableSide, e: PointerEvent) {
|
|
288
|
+
dragSide = side
|
|
289
|
+
dragStartX = e.clientX
|
|
290
|
+
dragStartW = parsePx(regions.value[side].size, 0)
|
|
291
|
+
document.body.style.userSelect = 'none'
|
|
292
|
+
window.addEventListener('pointermove', onResizeMove)
|
|
293
|
+
window.addEventListener('pointerup', onResizeUp)
|
|
294
|
+
}
|
|
295
|
+
|
|
209
296
|
// Published on the layout root so a fixed docked panel (ChatDock) can inset itself
|
|
210
297
|
// below the top band / above the bottom band:
|
|
211
298
|
// inset-block-start: var(--bgl-region-top); inset-block-end: var(--bgl-region-bottom);
|
|
@@ -246,6 +333,8 @@ onMounted(() => {
|
|
|
246
333
|
onUnmounted(() => {
|
|
247
334
|
window.removeEventListener('resize', onResize)
|
|
248
335
|
clearTimeout(resizeAnimTimer)
|
|
336
|
+
window.removeEventListener('pointermove', onResizeMove)
|
|
337
|
+
window.removeEventListener('pointerup', onResizeUp)
|
|
249
338
|
})
|
|
250
339
|
|
|
251
340
|
const slotBind = computed(() => ({ isOpen, isMobile, toggleMenu, closeOnMobile }))
|
|
@@ -259,23 +348,30 @@ const slotBind = computed(() => ({ isOpen, isMobile, toggleMenu, closeOnMobile }
|
|
|
259
348
|
<slot name="top" v-bind="{ ...slotBind, region: regions.top, setRegion: setTop }" />
|
|
260
349
|
</div>
|
|
261
350
|
|
|
262
|
-
<!-- The main row: start
|
|
263
|
-
|
|
351
|
+
<!-- The main row is a CSS grid: [start | center | end]. The parent owns every
|
|
352
|
+
track size (rowStyle → grid-template-columns); each rail just fills its
|
|
353
|
+
track. Overlay rails (mobile drawer, floating chat) collapse their track
|
|
354
|
+
to 0 and their fixed child floats above instead. -->
|
|
355
|
+
<div class="app-row relative" :class="{ 'app-layout--resizing': isResizing }" :style="rowStyle">
|
|
264
356
|
<!-- Mobile sidebar drawer backdrop -->
|
|
265
357
|
<div v-if="startOverlayOpen" class="overlay" @click="closeOnMobile" />
|
|
266
358
|
|
|
267
|
-
<!-- start
|
|
268
|
-
<
|
|
269
|
-
<slot name="
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
359
|
+
<!-- start rail. #sidebar = back-compat alias. -->
|
|
360
|
+
<div class="region-box region-box--start" :class="{ 'region-box--flow': isFlow('start') }">
|
|
361
|
+
<slot name="start" v-bind="{ ...slotBind, region: regions.start, setRegion: setStart }">
|
|
362
|
+
<slot name="sidebar" v-bind="slotBind" />
|
|
363
|
+
</slot>
|
|
364
|
+
<div
|
|
365
|
+
v-show="handleVisible('start')"
|
|
366
|
+
class="region-resize region-resize--start"
|
|
367
|
+
@pointerdown.prevent="onResizeDown('start', $event)"
|
|
368
|
+
>
|
|
369
|
+
<div class="region-resize-bar" />
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
276
372
|
|
|
277
|
-
<!-- Center content
|
|
278
|
-
<main class="main-content flex column
|
|
373
|
+
<!-- Center content — the minmax(0,1fr) track. -->
|
|
374
|
+
<main class="main-content flex column">
|
|
279
375
|
<!-- #header alias: AppContent-style per-page header (center-only). -->
|
|
280
376
|
<slot name="header" v-bind="slotBind" />
|
|
281
377
|
|
|
@@ -283,6 +379,20 @@ const slotBind = computed(() => ({ isOpen, isMobile, toggleMenu, closeOnMobile }
|
|
|
283
379
|
<slot v-bind="slotBind" />
|
|
284
380
|
</div>
|
|
285
381
|
</main>
|
|
382
|
+
|
|
383
|
+
<!-- end rail (aside / chat). #aside = back-compat alias. -->
|
|
384
|
+
<div class="region-box region-box--end" :class="{ 'region-box--flow': isFlow('end') }">
|
|
385
|
+
<slot name="end" v-bind="{ ...slotBind, region: regions.end, setRegion: setEnd }">
|
|
386
|
+
<slot name="aside" v-bind="{ isMobile }" />
|
|
387
|
+
</slot>
|
|
388
|
+
<div
|
|
389
|
+
v-show="handleVisible('end')"
|
|
390
|
+
class="region-resize region-resize--end"
|
|
391
|
+
@pointerdown.prevent="onResizeDown('end', $event)"
|
|
392
|
+
>
|
|
393
|
+
<div class="region-resize-bar" />
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
286
396
|
</div>
|
|
287
397
|
|
|
288
398
|
<!-- bottom region: full-width band below the start/end rails. -->
|
|
@@ -309,13 +419,67 @@ width: 100%;
|
|
|
309
419
|
z-index: 45;
|
|
310
420
|
}
|
|
311
421
|
|
|
312
|
-
/* The rail row
|
|
422
|
+
/* The rail row is a grid: [start | center | end]. Track sizes come from inline
|
|
423
|
+
`grid-template-columns` (rowStyle); animating it slides open/close + resize. */
|
|
313
424
|
.app-row {
|
|
314
425
|
flex: 1;
|
|
315
|
-
display:
|
|
426
|
+
display: grid;
|
|
316
427
|
min-height: 0;
|
|
317
428
|
overflow: hidden;
|
|
429
|
+
transition: grid-template-columns 300ms ease;
|
|
430
|
+
}
|
|
431
|
+
/* Suppress the track animation while the viewport is being resized (breakpoint cross). */
|
|
432
|
+
.app-row.app-layout--resizing {
|
|
433
|
+
transition: none;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* ── Region rails — grid tracks ───────────────────────────────────────────────
|
|
437
|
+
A start/end rail fills its grid track (the parent sizes the track). The child
|
|
438
|
+
fills the rail (100%). An overlay rail has a 0-width track — its fixed child
|
|
439
|
+
floats above instead, so this box is inert. */
|
|
440
|
+
.region-box {
|
|
441
|
+
position: relative;
|
|
442
|
+
min-width: 0;
|
|
443
|
+
min-height: 0;
|
|
444
|
+
display: flex;
|
|
445
|
+
flex-direction: column;
|
|
446
|
+
}
|
|
447
|
+
.region-box--flow {
|
|
448
|
+
overflow: hidden;
|
|
318
449
|
}
|
|
450
|
+
/* The child (AppSidebar/ChatDock) fills the rail entirely. */
|
|
451
|
+
.region-box--flow > :first-child {
|
|
452
|
+
width: 100%;
|
|
453
|
+
height: 100%;
|
|
454
|
+
min-height: 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* Region resize handles — a thin grip on the in-flow box's inner edge. */
|
|
458
|
+
.region-resize {
|
|
459
|
+
position: absolute;
|
|
460
|
+
inset-block: 0;
|
|
461
|
+
width: 10px;
|
|
462
|
+
z-index: 46;
|
|
463
|
+
cursor: ew-resize;
|
|
464
|
+
touch-action: none;
|
|
465
|
+
user-select: none;
|
|
466
|
+
display: flex;
|
|
467
|
+
align-items: center;
|
|
468
|
+
justify-content: center;
|
|
469
|
+
}
|
|
470
|
+
/* Sit on the seam facing the center: start's inner edge is its END side, and vice-versa. */
|
|
471
|
+
.region-resize--start { inset-inline-end: -5px; }
|
|
472
|
+
.region-resize--end { inset-inline-start: -5px; }
|
|
473
|
+
.region-resize-bar {
|
|
474
|
+
width: 3px;
|
|
475
|
+
height: 28px;
|
|
476
|
+
border-radius: 9999px;
|
|
477
|
+
background: var(--bgl-primary);
|
|
478
|
+
opacity: 0;
|
|
479
|
+
transition: opacity 0.15s;
|
|
480
|
+
}
|
|
481
|
+
.region-resize:hover .region-resize-bar,
|
|
482
|
+
.region-resize:active .region-resize-bar { opacity: 1; }
|
|
319
483
|
|
|
320
484
|
/* Overlay for mobile */
|
|
321
485
|
.overlay {
|
|
@@ -325,24 +489,13 @@ background: rgba(0, 0, 0, 0.5);
|
|
|
325
489
|
z-index: 40;
|
|
326
490
|
}
|
|
327
491
|
|
|
328
|
-
/* Main Content */
|
|
492
|
+
/* Main Content — the center grid track (minmax(0,1fr)). */
|
|
329
493
|
.main-content {
|
|
330
|
-
|
|
494
|
+
min-width: 0;
|
|
331
495
|
overflow: hidden;
|
|
332
|
-
/* Pushes when any push region opens/closes. Keep in sync with a panel's own
|
|
333
|
-
slide transition (e.g. ChatWindow) so they move together. */
|
|
334
|
-
transition: margin 300ms ease;
|
|
335
496
|
min-height: 0;
|
|
336
497
|
}
|
|
337
498
|
|
|
338
|
-
/* While the viewport is actively being resized, kill the push transitions so
|
|
339
|
-
crossing the mobile breakpoint doesn't animate the center content sliding
|
|
340
|
-
sideways. The animation still plays on an intentional sidebar toggle (when
|
|
341
|
-
`--resizing` is absent). */
|
|
342
|
-
.app-layout--resizing .main-content {
|
|
343
|
-
transition: none;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
499
|
/* Content gutter, breakpoint-aware:
|
|
347
500
|
• Desktop — `.page-content` applies the side gutter (as it always did), so every
|
|
348
501
|
view (with or without AppContent) is inset and aligned. An AppContent header
|