@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.
@@ -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
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.188",
4
+ "version": "1.15.192",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -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 0;
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
- const hasSidebar = computed(() => regions.value.start.present)
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
- Docked panels (ChatDock) position themselves fixed and read the published vars
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<{ 'update:open': [value: boolean] }>()
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
- const endHasContent = computed(() => slotHasContent(slots.end) || slotHasContent(slots.aside))
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
- // The sidebar overlays (drawer) on mobile → reserves nothing.
194
- const overlay = r.mode === 'overlay' || (side === 'start' && isMobile.value)
195
- // #end reserves nothing when its slot renders nothing (e.g. dock v-if'd away).
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
- // start/end are fixed side rails → reserved via inline margins.
202
- // top/bottom render in-flow inside <main> (flex column), so flexbox handles their
203
- // height automatically — no block-axis margin math (they inset only the center).
204
- const mainContentStyles = computed(() => ({
205
- marginInlineStart: reserved('start'),
206
- marginInlineEnd: reserved('end'),
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 rail | center | end rail. -->
263
- <div class="app-row relative">
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 region (sidebar). #sidebar is the back-compat alias. -->
268
- <slot name="start" v-bind="{ ...slotBind, region: regions.start, setRegion: setStart }">
269
- <slot name="sidebar" v-bind="slotBind" />
270
- </slot>
271
-
272
- <!-- end region (aside / chat). #aside is the back-compat alias. -->
273
- <slot name="end" v-bind="{ ...slotBind, region: regions.end, setRegion: setEnd }">
274
- <slot name="aside" v-bind="{ isMobile }" />
275
- </slot>
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, inset by the start/end push rails -->
278
- <main class="main-content flex column ease-300" :style="mainContentStyles">
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 fills the remaining height and holds start | center | end. */
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: flex;
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
- flex: 1;
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